

# Best Practices for Third-Party Syncing

In this chapter, you'll learn about best practices for syncing data between Medusa and third-party systems.

## Common Issues with Third-Party Syncing Implementation

Syncing data between Medusa and external systems is a common use case for commerce applications. For example, if your commerce ecosystem includes an external CMS or inventory management system, you may need to sync product data between Medusa and these systems.

However, how you implement third-party syncing can significantly impact performance and memory usage. Syncing large amounts of data without proper handling can lead to issues like:

- Out-of-memory (OOM) errors.
- Slow syncs that block the event loop and degrade application performance.
- Application crashes in production.

This chapter covers best practices to avoid these issues when syncing data between Medusa and third-party systems. These best practices are general programming patterns that aren't Medusa-specific, but they're essential for building robust third-party syncs.

***

## How to Sync Data Between Systems

Before diving into best practices, it's important to understand the general approach for syncing data between Medusa and third-party systems. Third-party syncing typically involves two main steps:

1. Define the syncing logic in a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md).
2. Execute the workflow from either a [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) or a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md).

### Define Syncing Logic in a Workflow

[Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) are special functions designed for long-running, asynchronous tasks. They provide features like [compensation](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md), [retries](https://docs.medusajs.com/learn/fundamentals/workflows/retry-failed-steps/index.html.md), and [async execution](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md) that are essential for reliable data syncing.

When defining your syncing logic, such as pushing product data to a third-party service or pulling inventory data into Medusa, you should define a workflow that encapsulates this logic.

Medusa also exposes [built-in workflows](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) for common commerce operations, like creating or updating products, that you can leverage in your syncing logic.

For example, you can use Medusa's built-in [batchProductsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/batchProductsWorkflow/index.html.md) to create or update products in batches:

```ts title="src/jobs/sync-products.ts"
import { MedusaContainer } from "@medusajs/framework/types"
import { batchProductsWorkflow } from "@medusajs/medusa/core-flows"

export default async function syncProductsJob(container: MedusaContainer) {
  // ...
  await batchProductsWorkflow(container).run({
    input: {
      create: productsToCreate,
      update: productsToUpdate,
    },
  })
}
```

### Execute Workflows from Scheduled Jobs or Subscribers

After defining your syncing logic in a workflow, or choosing a Medusa workflow to use, you need to execute it based on your syncing requirements:

- [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md): Use scheduled jobs for periodic syncs, such as syncing products daily or inventory hourly. Scheduled jobs run at specified intervals and can trigger your workflow to perform the sync.
- [Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md): Use subscribers for event-driven syncs, such as syncing data when a product is updated in Medusa or when an order is placed. Subscribers listen for specific events and can trigger your workflow in response.

If you've set up [server and worker instances](https://docs.medusajs.com/learn/production/worker-mode/index.html.md), the worker will handle the execution. So, the syncing execution won't block the main server process.

[Cloud](https://docs.medusajs.com/cloud/index.html.md) creates server and worker instances for your project automatically, so you don't need to set this up manually.

In the scheduled job or subscriber, you retrieve the data to be synced from the third-party service or from Medusa itself. Then, you execute the workflow, passing it the data to be synced.

For example, the following scheduled job fetches products from a third-party service and syncs them to Medusa using a workflow:

```ts title="src/jobs/sync-products.ts"
import { MedusaContainer } from "@medusajs/framework/types"
import { batchProductsWorkflow } from "@medusajs/medusa/core-flows"

export default async function syncProductsJob(container: MedusaContainer) {
  const productStream = streamProductsFromApi()
  const batchedProducts = batchProducts(productStream, 50)

  for await (const batch of batchedProducts) {
    const { 
      productsToCreate, 
      productsToUpdate,
    } = await prepareProducts(batch)
    
    await batchProductsWorkflow(container).run({
      input: {
        create: productsToCreate,
        update: productsToUpdate,
      },
    })
  }
}
```

You'll learn about best practices for implementing the data fetching, batching, and preparation logic in the next sections.

***

## Syncing Best Practices

The following sections cover best practices for implementing third-party syncing in a way that minimizes memory usage, maximizes performance, and ensures reliability.

### Full Scheduled Job Code

The following sections take snippets from this complete example of a high-performance, memory-efficient product synchronization job that incorporates all the best practices discussed:

```ts title="src/jobs/sync-products.ts"
import { MedusaContainer } from "@medusajs/framework/types"
import { ContainerRegistrationKeys, MedusaError, Modules } from "@medusajs/framework/utils"
import { batchProductsWorkflow } from "@medusajs/medusa/core-flows"
import { Readable } from "stream"
import { parser } from "stream-json"
import { pick } from "stream-json/filters/Pick"
import { streamArray } from "stream-json/streamers/StreamArray"
import { chain } from "stream-chain"

const API_FETCH_SIZE = 200
const PROCESS_BATCH_SIZE = 50
const MAX_RETRIES = 3
const RETRY_DELAY_MS = 1000
const FETCH_TIMEOUT_MS = 30000

async function fetchWithRetry(
  url: string,
  retries = MAX_RETRIES
): Promise<Response> {
  let lastError: Error | null = null

  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      const controller = new AbortController()
      const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)

      const response = await fetch(url, {
        signal: controller.signal,
        // Keep connection alive for efficiency with multiple requests
        headers: {
          "Connection": "keep-alive",
        },
      })

      clearTimeout(timeoutId)

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }

      return response
    } catch (error: any) {
      lastError = error
      const isRetryable =
        error.code === "UND_ERR_SOCKET" ||
        error.code === "ECONNREFUSED" ||
        error.code === "ECONNRESET" ||
        error.code === "ETIMEDOUT" ||
        error.name === "AbortError"

      if (isRetryable && attempt < retries) {
        // Exponential backoff: 1s → 2s → 4s
        const delay = RETRY_DELAY_MS * Math.pow(2, attempt - 1)
        await new Promise((resolve) => setTimeout(resolve, delay))
      } else {
        break
      }
    }
  }

  throw lastError
}

/**
 - Streams products from the external API using incremental JSON parsing
 - Products are yielded one by one as they're parsed from the response stream
 */
async function* streamProductsFromApi() {
  let offset = 0
  let hasMore = true

  while (hasMore) {
    const url = `https://third-party-api.com/products?limit=${API_FETCH_SIZE}&offset=${offset}`

    const response = await fetchWithRetry(url)

    // Convert web ReadableStream to Node.js Readable
    const nodeStream = Readable.fromWeb(response.body as any)

    // Create a streaming JSON parser pipeline that:
    // 1. Parses JSON incrementally
    // 2. Picks only the "products" array
    // 3. Streams each array item individually
    const pipeline = chain([
      nodeStream,
      parser(),
      pick({ filter: "products" }),
      streamArray(),
    ])

    let productCount = 0

    // Yield each product as it's parsed - memory stays constant
    try {
      for await (const { value } of pipeline) {
        yield value
        productCount++

        // Yield to event loop periodically to prevent blocking
        if (productCount % 100 === 0) {
          await new Promise((resolve) => setImmediate(resolve))
        }
      }
    } catch (streamError: any) {
      // Handle stream errors (socket closed mid-stream)
      if (streamError.code === "UND_ERR_SOCKET" || streamError.code === "ECONNRESET") {
        throw new MedusaError(
          MedusaError.Types.UNEXPECTED_STATE,
          `Stream interrupted after ${productCount} products: ${streamError.message}`
        )
      }
      throw streamError
    }

    // If the products are less than expected, there are no more products
    if (productCount < API_FETCH_SIZE) {
      hasMore = false
    } else {
      offset += productCount
    }
  }
}

/**
 - Collects products into batches of the specified size
 */
async function* batchProducts(
  products: AsyncGenerator,
  batchSize: number
): AsyncGenerator<any[]> {
  let batch: any[] = []

  for await (const product of products) {
    batch.push(product)

    if (batch.length >= batchSize) {
      yield batch
      // Release reference for GC
      batch = []
    }
  }

  // Yield remaining products
  if (batch.length > 0) {
    yield batch
  }
}

export default async function syncProductsJob(container: MedusaContainer) {
  const query = container.resolve(ContainerRegistrationKeys.QUERY)
  const workflowEngine = container.resolve(Modules.WORKFLOW_ENGINE)

  let totalCreated = 0
  let totalUpdated = 0
  let batchNumber = 0

  // Stream products from API and process in batches
  // Memory stays constant - we only hold PROCESS_BATCH_SIZE products at a time
  const productStream = streamProductsFromApi()
  const batchedProducts = batchProducts(productStream, PROCESS_BATCH_SIZE)

  for await (const batch of batchedProducts) {
    batchNumber++

    // Extract external IDs from this batch to look up in Medusa
    const externalIds = batch.map((p) => p.id)

    // Query Medusa for products matching these external IDs
    const { data: existingProducts } = await query.graph(
      {
        entity: "product",
        fields: ["id", "updated_at", "external_id"],
        filters: {
          external_id: externalIds,
        },
      }
    )

    // Build a map for quick lookup
    const existingByExternalId = new Map(
      existingProducts.map((p) => [p.external_id, { 
        id: p.id, 
        updatedAt: p.updated_at,
      }])
    )

    const productsToCreate: any[] = []
    const productsToUpdate: any[] = []

    for (const externalProduct of batch) {
      const existing = existingByExternalId.get(externalProduct.id)

      if (existing) {
        // Product exists - prepare update
        productsToUpdate.push({
          id: existing.id,
          title: externalProduct.title,
          description: externalProduct.description ?? undefined,
          metadata: {
            external_id: externalProduct.id,
            last_synced: new Date().toISOString(),
          },
        })
      } else {
        // New product - prepare create
        productsToCreate.push({
          title: externalProduct.title,
          description: externalProduct.description ?? undefined,
          handle: externalProduct.handle,
          status: "draft",
          metadata: {
            external_id: externalProduct.id,
            last_synced: new Date().toISOString(),
          },
          options: [
            {
              title: "Default",
              values: ["Default"],
            },
          ],
          variants: externalProduct.variants.map((v) => ({
            title: v.title ?? "Default Variant",
            sku: v.sku ?? undefined,
            options: {
              Default: "Default",
            },
            prices: [
              {
                amount: v.price,
                currency_code: "usd",
              },
            ],
          })),
        })
      }
    }

    // Execute batch workflow for this batch
    if (productsToCreate.length > 0 || productsToUpdate.length > 0) {
      await batchProductsWorkflow(container).run({
        input: {
          create: productsToCreate,
          update: productsToUpdate,
        },
      })

      totalCreated += productsToCreate.length
      totalUpdated += productsToUpdate.length
    }
    // Yield to event loop between batches
    await new Promise((resolve) => setImmediate(resolve))
  }
}

export const config = {
  name: "sync-products",
  schedule: "0 0 * * *", // Run at midnight every day
}
```

### Stream Data from External APIs

When retrieving data from external APIs using `fetch`, the common approach is to fetch the data and load it entirely into memory using `response.json()`. For example:

```ts
const response = await fetch("https://third-party-api.com/products")
// Load entire response into memory
const data = await response.json()
```

However, this can lead to high memory usage and performance issues, especially with large datasets.

Instead, process data in streams or batches. This approach allows you to handle large datasets without loading everything into memory at once. You can use libraries like [stream-json](https://www.npmjs.com/package/stream-json) to parse JSON data incrementally as it's received.

![Diagram showcasing object in memory when loading entire JSON response vs streaming JSON parsing](https://res.cloudinary.com/dza7lstvk/image/upload/v1764761087/Medusa%20Book/stream-vs-full_iz2fyv.jpg)

First, install the `stream-json` library in your Medusa project:

```bash npm2yarn
npm install stream-json @types/stream-json
```

Then, use it in your scheduled job or subscriber to stream and parse JSON data from the third-party service:

```ts title="src/jobs/sync-products.ts" highlights={streamDataHighlights}
import { Readable } from "stream"
import { parser } from "stream-json"
import { pick } from "stream-json/filters/Pick"
import { streamArray } from "stream-json/streamers/StreamArray"
import { chain } from "stream-chain"

const API_FETCH_SIZE = 200

async function* streamProductsFromApi() {
  let offset = 0
  let hasMore = true
  
  while (hasMore) {
    const url = `https://third-party-api.com/products?limit=${API_FETCH_SIZE}&offset=${offset}`
    // TODO: Add retry with exponential backoff
    const response = await fetch(url)
    
    // Convert web ReadableStream to Node.js Readable
    const nodeStream = Readable.fromWeb(response.body as any)

    // Create a streaming JSON parser pipeline that:
    // 1. Parses JSON incrementally
    // 2. Picks only the "products" array
    // 3. Streams each array item individually
    const pipeline = chain([
      nodeStream,
      parser(),
      pick({ filter: "products" }),
      streamArray(),
    ])

    let productCount = 0

    try {
      // Yield each product one at a time
      for await (const { value } of pipeline) {
        yield value
        productCount++

        // TODO: Yield to event loop periodically to prevent blocking
      }
    } catch (streamError: any) {
      // TODO: Handle stream errors
    }

    // If the products are less than expected, there are no more products
    if (productCount < API_FETCH_SIZE) {
      hasMore = false
    } else {
      offset += productCount
    }
  }
}

export default async function syncProductsJob(container: MedusaContainer) {
  const productStream = streamProductsFromApi()
  // ...
}
```

In the above snippet, you set up a streaming JSON parser that processes the API response incrementally. Instead of loading the entire response into memory, the parser yields each product one at a time.

This approach significantly reduces memory usage, as you only hold JSON tokens and the current product being parsed, rather than the entire dataset.

#### Handle Stream Errors

When working with streams, it's important to handle potential errors that may occur during streaming, such as network interruptions. Catch these errors and implement retry logic or error reporting as needed.

For example:

```ts title="src/jobs/sync-products.ts"
async function* streamProductsFromApi() {
  // Initial setup...
  while (hasMore) {
    // Setup pipeline...
    try {
      for await (const { value } of pipeline) {
        yield value
        // ...
      }
    } catch (streamError: any) {
      // Handle stream errors (socket closed mid-stream)
      if (
        streamError.code === "UND_ERR_SOCKET" || 
        streamError.code === "ECONNRESET"
      ) {
        throw new MedusaError(
          MedusaError.Types.UNEXPECTED_STATE,
          `Stream interrupted after ${productCount} products: ${streamError.message}`
        )
      }
      throw streamError
    }
    // Update pagination...
  }
}

export default async function syncProductsJob(container: MedusaContainer) {
  // Initial setup...

  const productStream = streamProductsFromApi()
  // sync products...
}
```

In the above snippet, you catch stream errors and check for specific error codes that indicate transient network issues. You can then decide how to handle these errors, such as retrying the fetch or logging the error.

### Retrieve Only Necessary Fields

A common performance pitfall when syncing data is retrieving more fields than necessary from third-party services or Medusa's [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). This leads to increased data size, slower performance, and higher memory usage.

When retrieving data from third-party services or with Medusa's Query, only request the necessary fields. Then, to efficiently group existing data for updates, use a [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) for quick lookups.

For example, don't retrieve all product fields like this:

```ts
// DON'T
const { data: existingProducts } = await query.graph(
  {
    entity: "product",
    fields: ["*"],
    filters: {
      external_id: externalIds,
    },
  }
)
```

Instead, only request the fields you need:

```ts title="src/jobs/sync-products.ts" highlights={fieldsHighlights}
export default async function syncProductsJob(container: MedusaContainer) {
  const query = container.resolve(ContainerRegistrationKeys.QUERY)
  
  // Initial setup...

  const productStream = streamProductsFromApi()
  const batchedProducts = batchProducts(productStream, PROCESS_BATCH_SIZE)

  for await (const batch of batchedProducts) {
    // Increment data...

    // Extract external IDs from this batch to look up in Medusa
    const externalIds = batch.map((p) => p.id)

    // Query Medusa for products matching these external IDs
    const { data: existingProducts } = await query.graph(
      {
        entity: "product",
        fields: ["id", "updated_at", "external_id"],
        filters: {
          external_id: externalIds,
        },
      }
    )

    // Build a map for quick lookup
    const existingByExternalId = new Map(
      existingProducts.map((p) => [p.external_id, { 
        id: p.id, 
        updatedAt: p.updated_at,
      }])
    )

    // Process batch and sync to Medusa...
  }
}
```

In the above snippet, after retrieving a batch of products from the external API, you query Medusa's products to find existing products that match the external IDs. You only request the necessary fields for this operation.

Then, you build a map `existingByExternalId` that enables efficient lookups when determining whether to create or update products.

This approach minimizes the amount of data transferred and processed, leading to better performance and lower memory usage.

### Use Async Generators

[Async generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator) are a powerful feature in JavaScript that allow you to define asynchronous iterators. They're particularly useful for processing large datasets incrementally, as they enable you to yield data items one at a time without loading everything into memory.

When retrieving large datasets from third-party services, use async generators to yield data items one at a time. This allows you to process data incrementally without loading everything into memory.

![Diagram showcasing how batches are loaded into memories one at a time with async generators](https://res.cloudinary.com/dza7lstvk/image/upload/v1764761706/Medusa%20Book/async-gen-batches_ne09bs.jpg)

For example:

```ts title="src/jobs/sync-products.ts" highlights={asyncGeneratorHighlights}
async function* streamProductsFromApi() {
  // Initial setup...

  while (hasMore) {
    // Setup pipeline...

    try {
      for await (const { value } of pipeline) {
        yield value // Yield one product at a time

        // TODO: Yield to event loop periodically to prevent blocking...
      }
    } catch (streamError: any) {
      // Handle stream errors...
    }

    // Update pagination...
  }
}

async function* batchProducts(
  products: AsyncGenerator,
  batchSize: number
): AsyncGenerator<any[]> {
  let batch: any[] = []

  for await (const product of products) {
    batch.push(product)

    if (batch.length >= batchSize) {
      yield batch
      // Release reference for GC
      batch = []
    }
  }

  // Yield remaining products
  if (batch.length > 0) {
    yield batch
  }
}

export default async function syncProductsJob(container: MedusaContainer) {
  // Initial setup...

  const productStream = streamProductsFromApi()
  const batchedProducts = batchProducts(productStream, PROCESS_BATCH_SIZE)

  for await (const batch of batchedProducts) {
    // Process batch and sync to Medusa...
  }
}
```

In the above snippet, you define two async generators:

1. `streamProductsFromApi`: Yields individual products from the third-party service one at a time.
2. `batchProducts`: Takes an async generator of products and yields them in batches of a specified size.

Then, in your scheduled job, you consume these generators using [for await...of](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of) loops to process product batches incrementally.

This approach keeps memory usage low, as you only hold the current product or batch in memory at any given time.

#### Release References for Garbage Collection

When using async generators, it's important to release references to processed data to allow for [garbage collection](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Memory_management#garbage_collection). This keeps memory usage low, especially when processing large datasets.

For example, the `batchProducts` generator demonstrates this:

```ts title="src/jobs/sync-products.ts" highlights={[["13"]]}
async function* batchProducts(
  products: AsyncGenerator,
  batchSize: number
): AsyncGenerator<any[]> {
  let batch: any[] = []

  for await (const product of products) {
    batch.push(product)

    if (batch.length >= batchSize) {
      yield batch
      // Release reference for GC
      batch = []
    }
  }

  // Yield remaining products
  if (batch.length > 0) {
    yield batch
  }
}
```

### Handle Backpressure

Backpressure is a mechanism that manages the flow of data between producers (API fetches) and consumers (data processing workflows). Without proper backpressure handling, fast API fetches can overwhelm the processing workflow, leading to high memory usage and potential crashes.

Handle backpressure by controlling the pace at which data is processed. One effective way to do this in JavaScript is using `for await...of` loops, which naturally provide backpressure by waiting for each iteration to complete before fetching the next item.

![Diagram showcasing timeline from start to end of scheduled job execution where batches are fetched and synced one at a time](https://res.cloudinary.com/dza7lstvk/image/upload/v1764762095/Medusa%20Book/timeline-backpressure_rtwwzt.jpg)

For example, you can implement backpressure handling in your scheduled job:

```ts title="src/jobs/sync-products.ts" highlights={[["7"]]}
export default async function syncProductsJob(container: MedusaContainer) {
  // Initial setup...

  const productStream = streamProductsFromApi()
  const batchedProducts = batchProducts(productStream, PROCESS_BATCH_SIZE)

  for await (const batch of batchedProducts) {
    // Process batch and sync to Medusa...
    // The next batch won't be fetched until processing of the current batch is complete
  }
}
```

In the above snippet, the `for await...of` loop processes each batch of products. This ensures that the next batch isn't fetched until the current batch has been fully processed, effectively implementing backpressure.

This approach keeps memory usage controlled and prevents the system from being overwhelmed by incoming data, ensuring stability during large data syncs.

### Retry Errors with Exponential Backoff

Errors can occur during data syncing due to transient network issues, rate limiting, or temporary unavailability of third-party services. To improve reliability, implement retry logic with exponential backoff for transient errors.

For example, implement a custom function that fetches data with retry logic, then use it to fetch data from the third-party service:

```ts title="src/jobs/sync-products.ts" highlights={retryHighlights}
const MAX_RETRIES = 3
const RETRY_DELAY_MS = 1000

async function fetchWithRetry(
  url: string,
  retries = MAX_RETRIES
): Promise<Response> {
  let lastError: Error | null = null

  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      // TODO: Add request timeout...
      const response = await fetch(url)

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }

      return response
    } catch (error: any) {
      lastError = error
      const isRetryable =
        error.code === "UND_ERR_SOCKET" ||
        error.code === "ECONNREFUSED" ||
        error.code === "ECONNRESET" ||
        error.code === "ETIMEDOUT" ||
        error.name === "AbortError"

      if (isRetryable && attempt < retries) {
        // Exponential backoff: 1s → 2s → 4s
        const delay = RETRY_DELAY_MS * Math.pow(2, attempt - 1)
        await new Promise((resolve) => setTimeout(resolve, delay))
      } else {
        break
      }
    }
  }

  throw lastError
}

async function* streamProductsFromApi() {
  // Initial setup...

  while (hasMore) {
    const url = `https://third-party-api.com/products?limit=${API_FETCH_SIZE}&offset=${offset}`

    const response = await fetchWithRetry(url)
    // TODO Setup pipeline...
  }
}

export default async function syncProductsJob(container: MedusaContainer) {
  // Initial setup...

  const productStream = streamProductsFromApi()
  // Process and sync products...
}
```

In the above snippet, the `fetchWithRetry` function attempts to fetch a URL multiple times if a retryable error occurs. It uses exponential backoff to increase the delay between retries, reducing the load on the third-party service.

This approach improves the reliability of your data syncing process by handling transient errors gracefully.

### Set Request Timeouts

When making API calls to third-party services, always set request timeouts. This prevents the event loop from being blocked indefinitely if the third-party service is unresponsive.

For example, set a request timeout on a `fetch` call using the `AbortController`:

```ts title="src/jobs/sync-products.ts" highlights={timeoutHighlights}
const FETCH_TIMEOUT_MS = 30000

async function fetchWithRetry(
  url: string,
  retries = MAX_RETRIES
): Promise<Response> {
  const lastError: Error | null = null

  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      const controller = new AbortController()
      const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)

      const response = await fetch(url, {
        signal: controller.signal,
        // Keep connection alive for efficiency with multiple requests
        headers: {
          "Connection": "keep-alive",
        },
      })

      clearTimeout(timeoutId)

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }

      return response
    } catch (error: any) {
      // Retry logic with exponential backoff...
    }
  }

  throw lastError
}

async function* streamProductsFromApi() {
  // initial setup...

  while (hasMore) {
    const url = `https://third-party-api.com/products?limit=${API_FETCH_SIZE}&offset=${offset}`

    const response = await fetchWithRetry(url)
    // Setup streaming pipeline...
  }
}

export default async function syncProductsJob(container: MedusaContainer) {
  // Initial setup...

  const productStream = streamProductsFromApi()
  // Process and sync products...
}
```

In the above snippet, an `AbortController` sets a timeout for the `fetch` request. If the request takes longer than the specified timeout, it's aborted, preventing indefinite blocking of the event loop.

This approach ensures that your application remains responsive even when third-party services are slow or unresponsive.

### Yield to the Event Loop

When processing large amounts of data in loops, periodically yield control back to the event loop. This prevents blocking the event loop for extended periods, which can lead to unresponsiveness in your application. For example, it may prevent other scheduled jobs or subscribers from executing.

Yield to the event loop using [setImmediate](https://nodejs.org/api/timers.html#timers_setimmediate_callback_args). For example:

```ts title="src/jobs/sync-products.ts" highlights={yieldHighlights}
async function* streamProductsFromApi() {
  // Initial setup...
  
  while (hasMore) {
    // Setup pipeline...

    try {
      // Yield each product one at a time
      for await (const { value } of pipeline) {
        yield value
        productCount++

        // Yield to event loop periodically to prevent blocking
        if (productCount % 100 === 0) {
          await new Promise((resolve) => setImmediate(resolve))
        }
      }
    } catch (streamError: any) {
      // Handle stream errors...
    }

    // If the products are less than expected, there are no more products
    if (productCount < API_FETCH_SIZE) {
      hasMore = false
    } else {
      offset += productCount
    }
  }
}

export default async function syncProductsJob(container: MedusaContainer) {
  const productStream = streamProductsFromApi()
  // Process and sync products...
}
```

In the above snippet, the code yields to the event loop every 100 products processed. This allows other tasks in the event loop to execute, improving overall responsiveness.


# Build Medusa Application

In this chapter, you'll learn how to create a production build of your Medusa application for deployment to a hosting provider.

The next chapter explains how to deploy your Medusa application.

The below instructions are useful for self-hosting your Medusa application. You can also use [Cloud](https://docs.medusajs.com/cloud/index.html.md) to deploy and manage your Medusa application without worrying about infrastructure. Medusa will handle the configurations, deployment, and scaling for you.

## build Command

The Medusa CLI tool provides a [build](https://docs.medusajs.com/resources/medusa-cli/commands/build/index.html.md) command that creates a standalone build of the Medusa application that:

- Doesn't rely on the source TypeScript files.
- Can be copied to a production server reliably.

So, to create the production build, run the following command in the root of your Medusa application:

```bash
npx medusa build
```

The command will create a `.medusa/server` directory in the root of your project that contains your build assets. The next sections explain how to use the build output.

***

## Start Built Medusa Application

To start the Medusa application after running the `build` command:

You need to run these steps every time you run the `build` command, since the `.medusa/server` directory is recreated each time.

1. Change to the `.medusa/server` directory and install the dependencies:

```bash npm2yarn
cd .medusa/server && npm install
```

2. When running the application locally, make sure to copy the `.env` file from the root project's directory. In production, use system environment variables instead.

```bash title=".medusa/server"
cp ../../.env .env.production
```

When `NODE_ENV=production`, the Medusa application loads the environment variables from `.env.production`. Learn more in the [environment variables guide](https://docs.medusajs.com/learn/fundamentals/environment-variables/index.html.md).

3. Set `NODE_ENV` to `production` in the system environment variable:

```bash title=".medusa/server"
export NODE_ENV=production
```

4. Start the Medusa application from `.medusa/server`:

```bash npm2yarn title=".medusa/server"
npm run start
```

### Authentication Locally in Production Build

When the Medusa application is started in production (`NODE_ENV=production`) or staging mode, cookie settings are stricter. Cookie authentication only works if the client application is served from the same domain as the Medusa server, and the domain is not `localhost` or part of the [public suffix list](https://publicsuffix.org/).

So if you try to access the Medusa Admin locally in production mode, you won't be able to log in.

To access the Medusa Admin locally in production mode, set the `projectConfig.cookieOptions` in your `medusa-config.ts` file to be less strict. For example:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    // ...
    cookieOptions: {
      sameSite: "lax",
      secure: false,
    },
  },
})
```

In this example, you set `sameSite` to `lax` and `secure` to `false`, which allows cookies to be sent over non-secure connections and from different domains.

Then rebuild the Medusa application and start it again as described in the [steps above](#start-built-medusa-application). You can now access the Medusa Admin locally in production mode.

Make sure to remove the `projectConfig.cookieOptions` configuration once you're done testing locally, as it's not secure for production environments.

***

## Build Output

The `build` command creates a `.medusa/server` directory in the root of your project that contains your build assets. Do not commit this directory to your repository. This directory should be generated as part of your deployment process.

The `.medusa/server` directory contains the following:

![.medusa/server directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1760709518/Medusa%20Book/output-dir_dlklk6.jpg)

- `public/admin`: Contains the production build of the admin dashboard. The `public` directory is publicly accessible.
- `src`: Contains the compiled JavaScript files of your `src` directory.
- `instrumentation.js`: The compiled [instrumentation configuration file](https://docs.medusajs.com/learn/debugging-and-testing/instrumentation/index.html.md), if you have one.
- `medusa-config.js`: The compiled Medusa configuration file.
- `package.json` and a lock file: The dependencies required to run the Medusa application in production.

### Built Assets

The `build` command compiles and copies `.{ts,js,tsx,jsx}` files in your project, such as those in the root directory and in the `src` directory.

If you have other assets, such as JSON files or fonts, that you need in your Medusa backend at runtime, you need to copy them to the `.medusa/server` directory as part of your build process.

For example, consider you have a `src/data` directory that contains JSON files needed at runtime. You can add a `postbuild` script to your `package.json` file to copy this directory after the build process:

```json title="package.json"
{
  "scripts": {
    "postbuild": "cp -r src/data .medusa/server/src/data",
    "build": "medusa build && npm run postbuild"
  }
}
```

This script copies the `src/data` directory to the `.medusa/server` directory after the build process.

Do not expose sensitive files in your deployments, especially in the `.medusa/server/public` directory, as they will be publicly accessible. Sensitive files include those containing API keys, database credentials, or any other confidential information.

### Customize Admin Build

The admin dashboard is built with [Vite](!https://vitejs.dev/). If you need to customize the build process to include additional static assets, you can modify the Vite configuration with the [admin.vite](https://docs.medusajs.com/learn/configurations/medusa-config/index.html.md) option in your `medusa-config.ts` file.

### Separate Admin Build

The `build` command accepts a `--admin-only` option that outputs the admin to the `.medusa/admin` directory. This is useful when deploying the admin dashboard separately, such as on Vercel:

```bash
npx medusa build --admin-only
```

***

## Deploying Production Build

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`


# Asymmetric Encryption

In this chapter, you'll learn how to configure asymmetric encryption in Medusa using public/private key pairs instead of a shared secret.

## What is Asymmetric Encryption?

By default, Medusa uses symmetric JWT authentication, where the same secret signs and verifies tokens. With asymmetric encryption, you use a private key to sign tokens and a public key to verify them.

This approach provides better security, supports key rotation, and enables distributed systems where multiple services can verify tokens without needing access to the signing key.

### When to Use Asymmetric Encryption

Asymmetric encryption is useful in several scenarios:

|Scenario|Description|Benefits|
|---|---|---|
|Multi-Instance Deployments|Running multiple Medusa instances behind a load balancer.|Centralized signing, reduced risk if an instance is compromised.|
|Microservices Architecture|Medusa as part of a larger microservices ecosystem.|Independent token verification across services.|
|JWKS Support|Dynamic key rotation using JSON Web Key Sets.|Seamless key rotation without service disruption.|

***

## How to Configure Asymmetric Encryption

### Step 1: Set Asymmetric JWT Configuration

To configure asymmetric encryption, you need to set up both signing and verification options in your `medusa-config.ts` file.

In `medusa-config.ts`, create a helper function to load the JWT configuration, and use it in the exported configuration:

```ts title="medusa-config.ts"
// other imports...
import jwt from "jsonwebtoken"

export const getJwtConfig = () => {
  return {
    jwtSecret: process.env.JWT_SECRET_KEY,
    jwtPublicKey: process.env.JWT_PUBLIC_KEY,
    jwtExpiresIn: process.env.JWT_EXPIRES_IN || "1d",
    jwtOptions: {
      algorithm: (process.env.JWT_ALGORITHM || "RS256") as jwt.Algorithm,
      audience: process.env.JWT_AUDIENCE
        ? process.env.JWT_AUDIENCE.split(",")
        : undefined,
      issuer: process.env.JWT_ISSUER,
      keyid: process.env.JWT_KEYID,
    },
    jwtVerifyOptions: {
      algorithms: [(process.env.JWT_ALGORITHM || "RS256") as jwt.Algorithm],
      audience: process.env.JWT_AUDIENCE
        ? process.env.JWT_AUDIENCE.split(",")
        : undefined,
      issuer: process.env.JWT_ISSUER,
    },
  }
}

const jwtConfig = getJwtConfig()

module.exports = defineConfig({
  projectConfig: {
    http: {
      // ...
      jwtSecret: jwtConfig.jwtSecret,
      jwtPublicKey: jwtConfig.jwtPublicKey,
      jwtExpiresIn: jwtConfig.jwtExpiresIn,
      jwtOptions: jwtConfig.jwtOptions,
      jwtVerifyOptions: jwtConfig.jwtVerifyOptions,
    },
    // ...
  },
  modules: [
    {
      resolve: "@medusajs/medusa/user",
      options: {
        jwt_secret: {
          key: jwtConfig.jwtSecret,
        },
        jwt_public_key: jwtConfig.jwtPublicKey,
        jwt_options: jwtConfig.jwtOptions,
      },
    },
  ],
})
```

You set the JWT configurations in the following options:

1. [http options](https://docs.medusajs.com/learn/configurations/medusa-config#http/index.html.md): You set the global JWT options for Medusa's HTTP layer, which are used to sign and verify JWT authentication tokens.
   - Refer to the [Medusa Configuration chapter](https://docs.medusajs.com/learn/configurations/medusa-config#http/index.html.md) for more details on these options and their default values.
2. [User Module options](https://docs.medusajs.com/resources/commerce-modules/user/module-options/index.html.md): You set the JWT options specific to the User Module, which are used to sign and verify invite tokens.

### Step 2: Generate Key Pair

Next, generate an RSA key pair (private and public keys) for signing and verifying tokens. You can use OpenSSL to generate the keys:

```bash
# Generate private key
openssl genrsa -out private-key.pem 2048

# Extract public key
openssl rsa -in private-key.pem -pubout -out public-key.pem
```

Make sure not to commit your private key to Git or any public repository. Add it to your `.gitignore` file to prevent accidental commits:

```title=".gitignore"
# Asymmetric encryption keys (DO NOT COMMIT)
private-key.pem
*.pem
```

### Step 3: Set Environment Variables

Finally, set the following environment variables using the generated keys:

```bash
# JWT Configuration
JWT_SECRET_KEY="-----BEGIN RSA PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC...
-----END RSA PRIVATE KEY-----"

JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvTtLGDIK...
-----END PUBLIC KEY-----"

JWT_ALGORITHM=RS256
JWT_EXPIRES_IN=1d
JWT_ISSUER=medusa
JWT_AUDIENCE=medusa-api
JWT_KEYID=medusa-key-1
```

Where:

- `JWT_SECRET_KEY`: Your RSA private key for signing tokens.
- `JWT_PUBLIC_KEY`: Your RSA public key for verifying tokens.
- `JWT_ALGORITHM`: The [signing algorithm](https://www.npmjs.com/package/jsonwebtoken#algorithms-supported).
- `JWT_EXPIRES_IN`: The token expiration time.
- `JWT_ISSUER`: (Optional) The issuer claim for tokens.
- `JWT_AUDIENCE`: (Optional) The audience claim for tokens.
- `JWT_KEYID`: (Optional) The key ID for JWKS support.

For multiline keys in `.env` files, wrap the key with double quotes and use `\n` for newlines.

***

## Using JWKS (JSON Web Key Set)

JWKS (JSON Web Key Set) is a set of public keys used to verify JWT tokens. By exposing a JWKS endpoint, you allow clients to dynamically fetch your public keys for token verification, enabling key rotation without requiring clients to update their configurations.

This section explains how to set up JWKS support in Medusa and verify tokens using JWKS.

### Step 1: Install Required Packages

First, install the packages for handling JWKS and JWT verification. Run the following command in your Medusa application:

```bash npm2yarn
npm install jwks-rsa jsonwebtoken
npm install @types/jsonwebtoken@8.5.9 --save-dev
```

You install the following packages:

1. `jwks-rsa`: A library to create a JWKS client that can fetch and cache public keys.
2. `jsonwebtoken`: A library to handle JWT token creation and verification.
3. `@types/jsonwebtoken`: TypeScript types for the `jsonwebtoken` package. Make sure to install version `8.5.9` for compatibility.

### Step 2: Expose JWKS Endpoint

To allow clients to fetch the JWKS, expose it in an [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md).

In the API route, return the JWKS content containing your public keys. You can set the JWKS content using an environment variable or by manually converting your public key to JWK format.

#### Option 1: Environment Variable

The first option is to set the JWKS content using an environment variable. Convert your public key to JWK format using online tools or libraries like [node-jose](https://www.npmjs.com/package/node-jose).

For example, add the following environment variable:

```bash
JWKS_CONTENT='{"keys":[{"kty":"RSA","use":"sig","kid":"medusa-key-1","n":"vTtLGDIK...","e":"AQAB"}]}'
```

Then, create the API route at `src/api/.well-known/jwks.json/route.ts` with the following content:

```ts title="src/api/.well-known/jwks.json/route.ts"
import type { 
  MedusaRequest, 
  MedusaResponse, 
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest, 
  res: MedusaResponse
) => {
  if (!process.env.JWKS_CONTENT) {
    return res.status(500).json({ error: "JWKS_CONTENT not configured" })
  }
  
  res.status(200).json(JSON.parse(process.env.JWKS_CONTENT))
}
```

This exposes your public key at `/.well-known/jwks.json`, which clients can fetch to verify tokens.

#### Option 2: Manual JWK Conversion

If you prefer not to use an environment variable, manually convert your public key to JWK format using the JWT configurations you set in `medusa-config.ts`.

For example, create the API route at `src/api/.well-known/jwks.json/route.ts` with the following content:

```ts title="src/api/.well-known/jwks.json/route.ts"
import type { 
  MedusaRequest, 
  MedusaResponse, 
} from "@medusajs/framework/http"
import crypto from "crypto"

export const GET = async (
  req: MedusaRequest, 
  res: MedusaResponse
) => {
  const configModule = req.scope.resolve("configModule")
  const { projectConfig } = configModule

  // If JWKS_CONTENT is set, use it
  if (process.env.JWKS_CONTENT) {
    return res.status(200).json(JSON.parse(process.env.JWKS_CONTENT))
  }

  // Otherwise, generate from public key
  const publicKey = projectConfig.http.jwtPublicKey
  if (!publicKey) {
    return res.status(500).json({ error: "No public key configured" })
  }

  try {
    // Convert PEM to JWK
    const jwk = crypto.createPublicKey(publicKey).export({ format: "jwk" })
    
    const jwks = {
      keys: [{
        ...jwk,
        use: "sig",
        kid: projectConfig.http.jwtOptions?.keyid || "medusa-key-1",
        alg: projectConfig.http.jwtOptions?.algorithm || "RS256",
      }],
    }

    res.status(200).json(jwks)
  } catch (error: any) {
    return res.status(500).json({ 
      error: "Failed to generate JWKS", 
      message: error.message, 
    })
  }
}
```

In the above example:

1. Check if the `JWKS_CONTENT` environment variable is set and return it if available.
2. If not, retrieve the public key from the Medusa configuration and convert it to JWK format using Node's `crypto` module.
3. Construct the JWKS response and return it.

The public key will be available at `/.well-known/jwks.json` for clients to fetch.

### Step 3: Verify Tokens Using JWKS

Finally, verify JWT tokens from incoming requests using the JWKS API route.

Create a [middleware](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) function at `src/api/middlewares/jwks-auth.ts` that uses the `jwks-rsa` package to fetch the public key and verify the token:

```ts title="src/api/middlewares/jwks-auth.ts"
import {
  MedusaRequest,
  MedusaNextFunction,
  MedusaResponse,
} from "@medusajs/framework/http"
import jwt from "jsonwebtoken"
import { JwksClient } from "jwks-rsa"

const MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL || "http://localhost:9000"

// Create JWKS client with caching
const jwksClient = new JwksClient({
  // This is the API route where your JWKS is exposed
  jwksUri: `${MEDUSA_BACKEND_URL}/.well-known/jwks.json`,
  cache: true,
  rateLimit: true,
  jwksRequestsPerMinute: 5,
  cacheMaxAge: 60 * 60 * 1000, // 1 hour in ms
})

// Helper to get signing key
async function getKey(header: any) {
  try {
    const key = await jwksClient.getSigningKey(header.kid)

    return key.getPublicKey()
  } catch (err: any) {
    throw new Error(`Failed to get signing key: ${err.message}`)
  }
}

// Function that validates JWT token
export async function isValidJWTToken(token: string): Promise<boolean> {
  if (!token) {
    return false
  }

  try {
    // Decode the token to get the header
    const decoded = jwt.decode(token, { complete: true }) as {
      header: { kid: string }
      payload: {
        actor_id: string
      }
    } | null

    if (
      !decoded ||
      !decoded.header ||
      !decoded.header.kid ||
      !decoded.payload.actor_id
    ) {
      return false
    }

    const publicKey = await getKey(decoded.header)

    return new Promise((resolve) => {
      jwt.verify(
        token,
        publicKey,
        {
          ignoreExpiration: false,
          ignoreNotBefore: false,
        },
        (err) => {
          if (err) {
            console.error("Error verifying JWT token:", err)
            resolve(false)
          }

          resolve(true)
        }
      )
    })
  } catch (err: any) {
    console.error("Error validating JWT token:", err)
    return false
  }
}

// Authentication middleware
export const jwtAuthMiddleware = async (
  req: MedusaRequest,
  res: MedusaResponse,
  next: MedusaNextFunction
) => {
  const authHeader = req.headers.authorization
  const jwtToken = authHeader?.split(" ")[1]

  // If we should check login and the JWT token is invalid, return 401
  if (!jwtToken || !(await isValidJWTToken(jwtToken))) {
    const error = new Error(
      "Invalid auth token provided"
    )

    return next(error)
  }

  return next()
}
```

The `jwtAuthMiddleware` function extracts the JWT token from the `Authorization` header, fetches the appropriate public key from the JWKS endpoint, and verifies the token.

Apply this middleware to your protected API routes to ensure that only requests with valid JWT tokens are allowed.

For example, apply the middleware in `src/api/middlewares.ts`:

```ts title="src/api/middlewares.ts"
import { defineMiddlewares } from "@medusajs/framework/http"
import { jwtAuthMiddleware } from "./middlewares/jwks-auth"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom-protected-route*",
      middlewares: [jwtAuthMiddleware],
    },
  ],
})
```

### Test JWKS Verification

To test the JWKS verification, start the Medusa application:

```bash npm2yarn
npm run dev
```

Next, obtain a valid JWT token by authenticating a user. For example, to authenticate an admin user, send a `POST` request to `/auth/user/emailpass`:

```bash
curl -X POST http://localhost:9000/auth/user/emailpass \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@medusa-test.com",
    "password": "supersecret"
  }'
```

Make sure to replace the email and password with valid credentials for your Medusa application.

The response will include a `token` field, which is your JWT token.

Finally, make a request to your protected route using the obtained JWT token:

```bash
curl http://localhost:9000/custom-protected-route \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"
```

If the token is valid, the middleware will successfully verify it using the public key fetched from the JWKS endpoint, and you'll receive a successful response. If the token is invalid or expired, you'll receive an error.


# 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.

## Configuration File

All configurations of the Medusa application are stored in the `medusa.config.ts` file. The file exports an object created using the `defineConfig` utility. For example:

```ts title="medusa.config.ts"
import { loadEnv, defineConfig } from "@medusajs/framework/utils"

loadEnv(process.env.NODE_ENV || "development", process.cwd())

module.exports = defineConfig({
  projectConfig: {
    databaseUrl: process.env.DATABASE_URL,
    http: {
      storeCors: process.env.STORE_CORS!,
      adminCors: process.env.ADMIN_CORS!,
      authCors: process.env.AUTH_CORS!,
      jwtSecret: process.env.JWT_SECRET || "supersecret",
      cookieSecret: process.env.COOKIE_SECRET || "supersecret",
    },
  },
})

```

The `defineConfig` utility accepts an object having the following properties:

- [projectConfig](#project-configurations-projectConfig): Essential configurations related to the Medusa application, such as database and CORS configurations.
- [admin](#admin-configurations-admin): Configurations related to the Medusa Admin.
- [modules](#module-configurations-modules): Configurations related to registered modules.
- [plugins](#plugin-configurations-plugins): Configurations related to registered plugins.
- [featureFlags](#feature-flags-featureFlags): Configurations to manage enabled beta features in the Medusa application.
- [logger](#custom-logger-logger): Override the default logger.

### Using Environment Variables

Notice that you use the `loadEnv` utility to load environment variables. Learn more about it in the [Environment Variables chapter](https://docs.medusajs.com/learn/fundamentals/environment-variables/index.html.md).

By using this utility, you can use environment variables as the values of your configurations. It's highly recommended that you use environment variables for secret values, such as API keys and database credentials, or for values that change based on the environment, such as the application's Cross Origin Resource Sharing (CORS) configurations.

For example, you can set the `DATABASE_URL` environment variable in your `.env` file:

```bash
DATABASE_URL=postgres://postgres@localhost/medusa-store
```

Then, use the value in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    databaseUrl: process.env.DATABASE_URL,
    // ...
  },
  // ...
})
```

***

## Project Configurations (`projectConfig`)

The `projectConfig` object contains essential configurations related to the Medusa application, such as database and CORS configurations.

### cookieOptions

This option is available since Medusa [v2.8.5](https://github.com/medusajs/medusa/releases/tag/v2.8.5).

The `projectConfig.cookieOptions` configuration defines cookie options to be passed to `express-session` when creating the session cookie. This configuration is useful when simulating a production environment locally, where you may need to set options like `secure` or `sameSite`. Learn more in the [Build chapter](https://docs.medusajs.com/learn/build#authentication-locally-in-production-build/index.html.md).

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    cookieOptions: {
      sameSite: "lax",
    },
    // ...
  },
  // ...
})
```

#### Properties

Aside from the following options, you can pass any property that the [express-session's cookie option accepts](https://www.npmjs.com/package/express-session).

- secure: (\`boolean\`) Whether the cookie should only be sent over HTTPS. This is useful in production environments where you want to ensure that cookies are only sent over secure connections.
- sameSite: (\`lax\` | \`strict\` | \`none\`) Controls the SameSite attribute of the cookie.
- maxAge: (\`number\`) The maximum age of the cookie in milliseconds set in the \`Set-Cookie\` header.
- httpOnly: (\`boolean\`) Whether to set the \`HttpOnly Set-Cookie\` attribute.
- priority: (\`low\` | \`medium\` | \`high\`) The value of the \[Priority Set-Cookie attribute]\(https://tools.ietf.org/html/draft-west-cookie-priority-00#section-4.1)
- domain: (\`string\`) The value of the \`Domain Set-Cookie\` attribute. By default, no domain is set, and most clients will consider the cookie to apply to the current domain only.
- path: (\`string\`) The value of the \`Path Set-Cookie\` attribute
- signed: (\`boolean\`) Whether to sign the cookie.

### databaseDriverOptions

The `projectConfig.databaseDriverOptions` configuration is an object of additional options used to configure the PostgreSQL connection. For example, you can support TLS/SSL connection using this configuration's `ssl` property.

This configuration is useful for production databases, which can be supported by setting the `rejectUnauthorized` attribute of `ssl` object to `false`. During development, it's recommended not to pass the `ssl.rejectUnauthorized` option.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    databaseDriverOptions: process.env.NODE_ENV !== "development" ?
      { connection: { ssl: { rejectUnauthorized: false } } } : {},
    // ...
  },
  // ...
})
```

When you disable `rejectUnauthorized`, make sure to also add `?ssl_mode=disable` to the end of the [databaseUrl](#databaseUrl) as well.

#### Properties

- connection: (\`object\`) An object of connection options.

  - ssl: (\`object\` | \`boolean\`) Either a boolean indicating whether to use SSL or an object of SSL options. You can find the full list of options in the \[Node.js documentation]\(https://nodejs.org/docs/latest-v20.x/api/tls.html#tlsconnectoptions-callback).

    - rejectUnauthorized: (boolean) Whether to reject unauthorized connections.

  - pool: (\`object\`) An object of options initialized by the underlying \[knex]\(https://knexjs.org/guide/#pool) client.

    - min: (\`number\`) The minimum number of connections in the pool.

    - max: (\`number\`) The maximum number of connections in the pool.

    - idleTimeoutMillis: (\`number\`) The maximum time, in milliseconds, that a connection can be idle before being released.

    - reapIntervalMillis: (\`number\`) How often to check for idle connections that can be released.

    - createRetryIntervalMillis: (\`number\`) How long to wait before retrying to create a connection after a failure.
- idle\_in\_transaction\_session\_timeout: (\`number\`) The maximum time, in milliseconds, that a session can be idle before being terminated.

### databaseLogging

The `projectConfig.databaseLogging` configuration specifies whether database messages should be logged to the console. It is `false` by default.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    databaseLogging: true,
    // ...
  },
  // ...
})
```

### databaseName

The `projectConfig.databaseName` configuration determines the name of the database to connect to. If the name is specified in the [databaseUrl](#databaseUrl) configuration, you don't have to use this configuration.

After setting the database credentials, you can create and setup the database using the [db:setup](https://docs.medusajs.com/resources/medusa-cli/commands/db#dbsetup/index.html.md) command of the Medusa CLI.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    databaseName: process.env.DATABASE_NAME ||
      "medusa-store",
    // ...
  },
  // ...
})
```

### databaseUrl

The `projectConfig.databaseUrl` configuration specifies the PostgreSQL connection URL of the database to connect to. Its format is:

```bash
postgres://[user][:password]@[host][:port]/[dbname]
```

Where:

- `[user]`: (required) your PostgreSQL username. If not specified, the system's username is used by default. The database user that you use must have create privileges. If you're using the `postgres` superuser, then it should have these privileges by default. Otherwise, make sure to grant your user create privileges. You can learn how to do that in [PostgreSQL's documentation](https://www.postgresql.org/docs/current/ddl-priv.html).
- `[:password]`: an optional password for the user. When provided, make sure to put `:` before the password.
- `[host]`: (required) your PostgreSQL host. When run locally, it should be `localhost`.
- `[:port]`: an optional port that the PostgreSQL server is listening on. By default, it's `5432`. When provided, make sure to put `:` before the port.
- `[dbname]`: the name of the database. If not set, then you must provide the database name in the [databaseName](#databasename) configuration.

You can learn more about the connection URL format in [PostgreSQL’s documentation](https://www.postgresql.org/docs/current/libpq-connect.html).

After setting the database URL, you can create and setup the database using the [db:setup](https://docs.medusajs.com/resources/medusa-cli/commands/db#dbsetup/index.html.md) command of the Medusa CLI.

#### Example

For example, set the following database URL in your environment variables:

```bash
DATABASE_URL=postgres://postgres@localhost/medusa-store
```

Then, use the value in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    databaseUrl: process.env.DATABASE_URL,
    // ...
  },
  // ...
})
```

### http

The `http` configures the application's http-specific settings, such as the JWT secret, CORS configurations, and more.

#### http.jwtOptions

The `projectConfig.http.jwtOptions` configuration specifies options for the JWT token when using asymmetric signing private/public key. These options will be used for validation if `jwtVerifyOptions` is not provided.

This configuration accepts an object with the same properties as the [jsonwebtoken sign options](https://www.npmjs.com/package/jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback).

Learn more in the [Asymmetric Encryption chapter](https://docs.medusajs.com/learn/configurations/medusa-config/asymmetric-encryption/index.html.md).

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      jwtOptions: {
        algorithm: "RS256",
        expiresIn: "1h",
        issuer: "medusa",
        keyid: "medusa",
      },
    },
    // ...
  },
  // ...
})
```

#### http.jwtPublicKey

The `projectConfig.http.jwtPublicKey` configuration specifies the public key used to verify the JWT token in combination with the JWT secret and the JWT options. This is only used when the JWT secret is a secret key for asymmetric validation.

It accepts one of the following values:

- A string containing the public key in PEM format.
- A `Buffer` containing the public key in PEM format.
- An object containing the following properties:
  - `key`: A string or `Buffer` containing the public key in PEM format.
  - `passphrase`: A string containing the passphrase for the public key, if applicable.

Learn more in the [Asymmetric Encryption chapter](https://docs.medusajs.com/learn/configurations/medusa-config/asymmetric-encryption/index.html.md).

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      jwtPublicKey: process.env.JWT_PUBLIC_KEY,
    },
    // ...
  },
  // ...
})
```

#### http.jwtSecret

The `projectConfig.http.jwtSecret` configuration is a random string used to create authentication tokens in the HTTP layer. This configuration is not required in development, but must be set in production.

In a development environment, if this option is not set the default value is `supersecret`. However, in production, if this configuration is not set, an error is thrown and the application crashes. This is to ensure that you set a secure value for the JWT secret in production.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      jwtSecret: process.env.JWT_SECRET || "supersecret",
    },
    // ...
  },
  // ...
})
```

#### http.jwtVerifyOptions

The `projectConfig.http.jwtVerifyOptions` configuration specifies options for the JWT token when using asymmetric validation private/public key.

This configuration accepts the same options as the [jsonwebtoken verify options](https://www.npmjs.com/package/jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback).

Learn more in the [Asymmetric Encryption chapter](https://docs.medusajs.com/learn/configurations/medusa-config/asymmetric-encryption/index.html.md).

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      jwtVerifyOptions: {
        algorithms: ["RS256"],
        issuer: "medusa",
      },
    },
    // ...
  },
  // ...
})
```

#### http.jwtExpiresIn

The `projectConfig.http.jwtExpiresIn` configuration specifies the expiration time for the JWT token. Its value format is based off the [ms package](https://github.com/vercel/ms).

If not provided, the default value is `1d`.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      jwtExpiresIn: process.env.JWT_EXPIRES_IN || "2d",
    },
    // ...
  },
  // ...
})
```

#### http.cookieSecret

The `projectConfig.http.cookieSecret` configuration is a random string used to sign cookies in the HTTP layer. This configuration is not required in development, but must be set in production.

In a development environment, if this option is not set the default value is `supersecret`. However, in production, if this configuration is not set, an error is thrown and the application crashes. This is to ensure that you set a secure value for the cookie secret in production.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      cookieSecret: process.env.COOKIE_SECRET || "supersecret",
    },
    // ...
  },
  // ...
})
```

#### http.authCors

The `projectConfig.http.authCors` configuration specifies the accepted URLs or patterns for API routes starting with `/auth`. It can either be one accepted origin, or a comma-separated list of accepted origins.

Every origin in that list must either be:

- A full URL. For example, `http://localhost:7001`. The URL must not end with a backslash;
- Or a regular expression pattern that can match more than one origin. For example, `.example.com`. The regex pattern that Medusa tests for is `^([\/~@;%#'])(.*?)\1([gimsuy]*)$`.

Since the `/auth` routes are used for authentication for both store and admin routes, it's recommended to set this configuration's value to a combination of the [storeCors](#httpstoreCors) and [adminCors](#httpadminCors) configurations.

Some example values of common use cases:

```bash
# Allow different ports locally starting with 700
AUTH_CORS=/http:\/\/localhost:700\d+$/

# Allow any origin ending with vercel.app. For example, admin.vercel.app
AUTH_CORS=/vercel\.app$/

# Allow all HTTP requests
AUTH_CORS=/http:\/\/.+/
```

Then, set the configuration in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      authCors: process.env.AUTH_CORS,
    },
    // ...
  },
  // ...
})
```

If you’re adding the value directly within `medusa-config.ts`, make sure to add an extra escaping `/` for every backslash in the pattern. For example:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      authCors: "/http:\\/\\/localhost:700\\d+$/",
    },
    // ...
  },
  // ...
})
```

#### http.storeCors

The `projectConfig.http.storeCors` configuration specifies the accepted URLs or patterns for API routes starting with `/store`. It can either be one accepted origin, or a comma-separated list of accepted origins.

Every origin in that list must either be:

- A full URL. For example, `http://localhost:7001`. The URL must not end with a backslash;
- Or a regular expression pattern that can match more than one origin. For example, `.example.com`. The regex pattern that Medusa tests for is `^([\/~@;%#'])(.*?)\1([gimsuy]*)$`.

Some example values of common use cases:

```bash
# Allow different ports locally starting with 800
STORE_CORS=/http:\/\/localhost:800\d+$/

# Allow any origin ending with vercel.app. For example, storefront.vercel.app
STORE_CORS=/vercel\.app$/

# Allow all HTTP requests
STORE_CORS=/http:\/\/.+/
```

Then, set the configuration in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      storeCors: process.env.STORE_CORS,
    },
    // ...
  },
  // ...
})
```

If you’re adding the value directly within `medusa-config.ts`, make sure to add an extra escaping `/` for every backslash in the pattern. For example:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      storeCors: "/vercel\\.app$/",
    },
    // ...
  },
  // ...
})
```

#### http.adminCors

The `projectConfig.http.adminCors` configuration specifies the accepted URLs or patterns for API routes starting with `/admin`. It can either be one accepted origin, or a comma-separated list of accepted origins.

Every origin in that list must either be:

- A full URL. For example, `http://localhost:7001`. The URL must not end with a backslash;
- Or a regular expression pattern that can match more than one origin. For example, `.example.com`. The regex pattern that Medusa tests for is `^([\/~@;%#'])(.*?)\1([gimsuy]*)$`.

Some example values of common use cases:

```bash
# Allow different ports locally starting with 700
ADMIN_CORS=/http:\/\/localhost:700\d+$/

# Allow any origin ending with vercel.app. For example, admin.vercel.app
ADMIN_CORS=/vercel\.app$/

# Allow all HTTP requests
ADMIN_CORS=/http:\/\/.+/
```

Then, set the configuration in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      adminCors: process.env.ADMIN_CORS,
    },
    // ...
  },
  // ...
})
```

If you’re adding the value directly within `medusa-config.ts`, make sure to add an extra escaping `/` for every backslash in the pattern. For example:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      adminCors: "/vercel\\.app$/",
    },
    // ...
  },
  // ...
})
```

#### http.compression

The `projectConfig.http.compression` configuration modifies the HTTP compression settings at the application layer. If you have access to the HTTP server, the recommended approach would be to enable it there. However, some platforms don't offer access to the HTTP layer and in those cases, this is a good alternative.

If you enable HTTP compression and you want to disable it for specific API Routes, you can pass in the request header `"x-no-compression": true`. Learn more in the [API Reference](https://docs.medusajs.com/api/store#http-compression).

For example:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      compression: {
        enabled: true,
        level: 6,
        memLevel: 8,
        threshold: 1024,
      },
    },
    // ...
  },
  // ...
})
```

This configuation is an object that accepts the following properties:

- enabled: (\`boolean\`) Whether to enable HTTP compression.
- level: (\`number\`) The level of zlib compression to apply to responses. A higher level will result in better compression but will take longer to complete. A lower level will result in less compression but will be much faster.
- memLevel: (\`number\`) How much memory should be allocated to the internal compression state. It value is between \`1\` (minimum level) and \`9\` (maximum level).
- threshold: (\`number\` | \`string\`) The minimum response body size that compression is applied on. Its value can be the number of bytes or any string accepted by the \[bytes]\(https://www.npmjs.com/package/bytes) package.

#### http.authMethodsPerActor

The `projectConfig.http.authMethodsPerActor` configuration specifies the supported authentication providers per actor type (such as `user`, `customer`, or any custom actor).

For example, you can allow Google login for `customers`, and allow email/password logins for `users` in the admin.

`authMethodsPerActor` is a an object whose key is the actor type (for example, `user`), and the value is an array of supported auth provider IDs (for example, `emailpass`).

Learn more about actor types in the [Auth Identity and Actor Type documentation](https://docs.medusajs.com/resources/commerce-modules/auth/auth-identity-and-actor-types/index.html.md).

For example:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      authMethodsPerActor: {
        user: ["emailpass"],
        customer: ["emailpass", "google"],
      },
    },
    // ...
  },
  // ...
})
```

The above configurations allow admin users to login using email/password, and customers to login using email/password and Google.

#### http.restrictedFields

The `projectConfig.http.restrictedFields` configuration specifies the fields that can't be selected in API routes (using the `fields` query parameter) unless they're allowed in the [request's Query configurations](https://docs.medusajs.com/learn/fundamentals/module-links/query#request-query-configurations/index.html.md). This is useful to restrict sensitive fields from being exposed in the API.

For example, you can restrict selecting customers in store API routes:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      restrictedFields: {
        store: ["customer", "customers"],
      },
    },
    // ...
  },
  // ...
})
```

The `restrictedFields` configuration accepts the following properties:

- store: (\`string\[]\`) An array of fields that can't be selected in store API routes.

### redisOptions

The `projectConfig.redisOptions` configuration defines options to pass to `ioredis`, which creates the Redis connection used to store the Medusa server session. Refer to [ioredis’s RedisOptions documentation](https://redis.github.io/ioredis/index.html#RedisOptions)
for the list of available options.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    redisOptions: {
      connectionName: process.env.REDIS_CONNECTION_NAME ||
        "medusa",
    },
    // ...
  },
  // ...
})
```

### redisPrefix

The `projectConfig.redisPrefix` configuration defines a prefix on all keys stored in Redis for the Medusa server session. The default value is `sess:`.

The value of this configuration is prepended to `sess:`. For example, if you set it to `medusa:`, then a key stored in Redis is prefixed by `medusa:sess`.

This configuration is not used for modules that also connect to Redis, such as the [Redis Caching Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/caching/providers/redis/index.html.md).

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    redisPrefix: process.env.REDIS_URL || "medusa:",
    // ...
  },
  // ...
})
```

### redisUrl

The `projectConfig.redisUrl` configuration specifies the connection URL to Redis to store the Medusa server session. When specified, the Medusa server uses Redis to store the session data. Otherwise, the session data is stored in-memory.

This configuration is not used for modules that also connect to Redis, such as the [Redis Caching Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/caching/providers/redis/index.html.md). You'll have to configure the Redis connection for those modules separately.

You must first have Redis installed. You can refer to [Redis's installation guide](https://redis.io/docs/getting-started/installation/).

The Redis connection URL has the following format:

```bash
redis[s]://[[username][:password]@][host][:port][/db-number]
```

Where:

- `redis[s]`: the protocol used to connect to Redis. Use `rediss` for a secure connection.
- `[[username][:password]@]`: an optional username and password for the Redis server.
- `[host]`: the host of the Redis server. When run locally, it should be `localhost`.
- `[:port]`: an optional port that the Redis server is listening on. By default, it's `6379`.
- `[/db-number]`: an optional database number to connect to. By default, it's `0`.

For a local Redis installation, the connection URL should be `redis://localhost:6379` unless you’ve made any changes to the Redis configuration during installation.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    redisUrl: process.env.REDIS_URL ||
      "redis://localhost:6379",
    // ...
  },
  // ...
})
```

### sessionOptions

The `projectConfig.sessionOptions` configuration defines additional options to pass to [express-session](https://www.npmjs.com/package/express-session), which is used to store the Medusa server session.

This configuration is not used for modules that also connect to Redis, such as the [Redis Caching Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/caching/providers/redis/index.html.md).

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    sessionOptions: {
      name: process.env.SESSION_NAME || "custom",
    },
    // ...
  },
  // ...
})
```

#### Properties

- name: (\`string\`) The name of the session ID cookie to set in the response (and read from in the request). Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#name) for more details.
- resave: (\`boolean\`) Whether the session should be saved back to the session store, even if the session was never modified during the request. Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#resave) for more details.
- rolling: (\`boolean\`) Whether the session identifier cookie should be force-set on every response. Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#rolling) for more details.
- saveUninitialized: (\`boolean\`) Whether to save sessions that are new but not modified. Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#saveUninitialized) for more details.
- secret: (\`string\`) The secret to sign the session ID cookie. By default, the value of \[http.cookieSecret]\(#httpcookieSecret) is used. Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#secret) for details.
- ttl: (\`number\`) The time-to-live (TTL) of the session ID cookie in milliseconds. It is used when calculating the \`Expires\` \`Set-Cookie\` attribute of cookies. Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#cookie) for more details.

### workerMode

The `projectConfig.workerMode` configuration specifies the worker mode of the Medusa application. You can learn more about it in the [Worker Mode chapter](https://docs.medusajs.com/learn/production/worker-mode/index.html.md).

The value for this configuration can be one of the following:

- `shared`: run the application in a single process, meaning the worker and server run in the same process.
- `worker`: run the a worker process only.
- `server`: run the application server only.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    workerMode: process.env.WORKER_MODE as "shared" | "worker" | "server" || "shared",
    // ...
  },
  // ...
})
```

***

## Admin Configurations (`admin`)

The `admin` object contains configurations related to the Medusa Admin.

### backendUrl

The `admin.backendUrl` configuration specifies the URL of the Medusa application. Its default value is the browser origin. This is useful to set when running the admin on a separate domain.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  admin: {
    backendUrl: process.env.MEDUSA_BACKEND_URL ||
      "http://localhost:9000",
  },
  // ...
})
```

### disable

The `admin.disable` configuration specifies whether to disable the Medusa Admin. If disabled, the Medusa Admin will not be compiled and you can't access it at `/app` path of your application. The default value is `false`.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  admin: {
    disable: process.env.ADMIN_DISABLED === "true" ||
      false,
  },
  // ...
})
```

### path

The `admin.path` configuration indicates the path to the admin dashboard, which is `/app` by default. The value must start with `/` and can't end with a `/`.

The value cannot be one of the reserved paths:

- `/admin`
- `/store`
- `/auth`
- `/`

When using Docker, make sure that the root path of the Docker image isn't the same as the admin's path. For example, if the Docker image's root path is `/app`, change
the value of the `admin.path` configuration, since it's `/app` by default.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  admin: {
    path: process.env.ADMIN_PATH || `/app`,
  },
  // ...
})
```

### storefrontUrl

The `admin.storefrontUrl` configuration specifies the URL of the Medusa storefront application. This URL is used as a prefix to some links in the admin that require performing actions in the storefront.

For example, this URL is used as a prefix to shareable payment links for orders with outstanding amounts.

#### Example

```js title="medusa-config.js"
module.exports = defineConfig({
  admin: {
    storefrontUrl: process.env.MEDUSA_STOREFRONT_URL ||
      "http://localhost:8000",
  },
  // ...
})
```

### vite

The `admin.vite` configration specifies Vite configurations for the Medusa Admin. Its value is a function that receives the default Vite configuration and returns the modified configuration. The default value is `undefined`.

Learn about configurations you can pass to Vite in [Vite's documentation](https://vite.dev/config/).

#### Example

For example, if you're using a third-party library that isn't ESM-compatible, add it to Vite's `optimizeDeps` configuration:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  admin: {
    vite: (config) => {
      return {
        ...config,
        optimizeDeps: {
          include: ["qs"],
        },
      }
    },
  },
  // ...
})
```

***

## Module Configurations (`modules`)

The `modules` configuration allows you to register and configure the [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) registered in the Medusa application. Medusa's commerce and Infrastructure Modules are configured by default. So, you only need to pass your custom modules, or override the default configurations of the existing modules.

`modules` is an array of objects for the modules to register. Each object has the following properties:

1. `resolve`: a string indicating the path to the module, or the module's NPM package name. For example, `./src/modules/my-module`.
2. `options`: (optional) an object indicating the [options to pass to the module](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md). This object is specific to the module and its configurations. For example, your module may require an API key option, which you can pass in this object.

For modules that are part of a plugin, learn about registering them in the [Register Modules in Plugins](#register-modules-in-plugins) section.

### Example

To register a custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  modules: [
    {
      resolve: "./src/modules/cms",
      options: {
        apiKey: process.env.CMS_API_KEY,
      },
    },
  ],
  // ...
})
```

You can also override the default configurations of Medusa's modules. For example, to add a Notification Module Provider to the Notification Module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  modules: [
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          // default provider
          {
            resolve: "@medusajs/medusa/notification-local",
            id: "local",
            options: {
              name: "Local Notification Provider",
              channels: ["feed"],
            },
          },
          // custom provider
          {
            resolve: "./src/modules/my-notification",
            id: "my-notification",
            options: {
              channels: ["email"],
              // provider options...
            },
          },
        ],
      },
    },
  ],
  // ...
})
```

***

## Plugin Configurations (`plugins`)

The `plugins` configuration allows you to register and configure the [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) registered in the Medusa application. Plugins include re-usable Medusa customizations, such as modules, workflows, API routes, and more.

Aside from installing the plugin with NPM, you must also register it in the `medusa.config.ts` file.

The `plugins` configuration is an array of objects for the plugins to register. Each object has the following properties:

- A string, which is the name of the plugin's package as specified in the plugin's `package.json` file. This is useful if the plugin doesn't require any options.
- An object having the following properties:
  - `resolve`: The name of the plugin's package as specified in the plugin's `package.json` file.
  - `options`: An object that includes [options to be passed to the modules](https://docs.medusajs.com/learn/fundamentals/modules/options#pass-options-to-a-module-in-a-plugin/index.html.md) within the plugin.

### Example

```ts title="medusa-config.ts"
module.exports = {
  plugins: [
    `medusa-my-plugin-1`,
    {
      resolve: `medusa-my-plugin`,
      options: {
        apiKey: process.env.MY_API_KEY ||
          `test`,
      },
    },
    // ...
  ],
  // ...
}
```

The above configuration registers two plugins: `medusa-my-plugin-1` and `medusa-my-plugin`. The latter plugin requires an API key option, which is passed in the `options` object.

### Register Modules in Plugins

When you register a plugin, its modules are automatically registered in the Medusa application. You don't have to register them manually in the `modules` configuration.

However, this isn't the case for module providers. If your plugin includes a module provider, you must register it in the `modules` configuration, referencing the module provider's path.

For example:

```ts title="medusa-config.ts"
module.exports = {
  plugins: [
    `medusa-my-plugin`,
  ],
  modules: [
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          // ...
          {
            resolve: "medusa-my-plugin/providers/my-notification",
            id: "my-notification",
            options: {
              channels: ["email"],
              // provider options...
            },
          },
        ],
      },
    },
  ],
  // ...
}
```

***

## Feature Flags (`featureFlags`)

The `featureFlags` configuration allows you to manage enabled beta features in the Medusa application.

Learn more in the [Feature Flags](https://docs.medusajs.com/learn/debugging-and-testing/feature-flags/index.html.md) chapter.

### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  featureFlags: {
    index_engine: true,
    // ...
  },
  // ...
})
```

***

## Custom Logger (`logger`)

The `logger` configuration allows you to override the default [Logger](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) used in the Medusa application with your custom implementation.

Learn more about creating a custom logger in the [Override Logger](https://docs.medusajs.com/learn/debugging-and-testing/logging/custom-logger/index.html.md) chapter.

The `logger` configuration accepts an instance of a class that extends the `Logger` interface.

### Example

```ts title="medusa-config.ts"
import { logger } from "./src/logger/my-logger"

module.exports = defineConfig({
  // ...
  logger,
})
```


# Using TypeScript Aliases

In this chapter, you'll learn how to use TypeScript aliases in your Medusa application.

## Support for TypeScript Aliases

By default, Medusa doesn't support TypeScript aliases in production. That means you may get build errors in production if you use them in your development.

If you prefer using TypeScript aliases, this section will guide you through the steps to enable them in your Medusa application.

### Step 1: Install Required Dependencies

Start by installing the following development dependencies:

```bash npm2yarn
npm install --save-dev tsc-alias rimraf
```

Where:

- `tsc-alias` resolves TypeScript aliases.
- `rimraf` removes files and directories.

### Step 2: Update `package.json`

Then, add a new `resolve:aliases` script to your `package.json` and update the existing `build` script:

```json title="package.json"
{
  "scripts": {
    // other scripts...
    "resolve:aliases": "tsc --showConfig -p tsconfig.json > tsconfig.resolved.json && tsc-alias -p tsconfig.resolved.json && rimraf tsconfig.resolved.json",
    "build": "medusa build && npm run resolve:aliases"
  }
}
```

### Step 3: Update `tsconfig.json`

Next, configure the TypeScript aliases you want to use in your `tsconfig.json` file by adding a `paths` property under `compilerOptions`.

For example, to import anything under the `src` directory using type aliases, add the following in `tsconfig.json`:

```json title="tsconfig.json"
{
  "compilerOptions": {
    // ...
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
```

### Step 4: Use TypeScript Aliases

Then, you can use the `@` alias in your application code.

For example, if you have a service in `src/modules/brand/service.ts`, you can import it like this:

```ts
import { BrandModuleService } from "@/modules/brand/service"
```

***

## Support TypeScript Aliases for Admin Customizations

Medusa also doesn't support TypeScript aliases in the admin customizations by default. However, you can also configure your Medusa application to use TypeScript aliases in your admin customizations.

### Step 1: Update `src/admin/tsconfig.json`

Update `src/admin/tsconfig.json` to include `baseUrl` and `paths` configuration:

```json title="src/admin/tsconfig.json"
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./*"] 
    }

    // other options...
  }
}
```

The `baseUrl` option sets the base directory to `src/admin`, and the `paths` option defines the `@` alias to allow importing files from the `src/admin` directory using aliases.

### Step 2: Update `medusa-config.ts`

Next, update the `vite` configuration in `medusa-config.ts` to include the `resolve.alias` configuration:

```ts title="medusa-config.ts"
import path from "path"

module.exports = defineConfig({
  // ...
  admin: {
    vite: () => ({
      resolve: {
        alias: {
          "@": path.resolve(__dirname, "./src/admin"),
        },
      },
    }),
  },
})
```

Learn more about the `vite` configuration in the [Medusa configuration](https://docs.medusajs.com/learn/configurations/medusa-config/index.html.md) chapter.

### Step 3: Use TypeScript Aliases in Admin Customizations

You can now use the `@` alias in your admin customizations, just like you do in your main application code.

For example, if you have a component in `src/admin/components/Container.tsx`, you can import it in a widget like this:

```ts
import Container from "@/components/Container"
```

### Match TSConfig and Vite Alias Configuration

Make sure that the `@` alias points to the same path as in your `src/admin/tsconfig.json`.

For example, if you set the `@/*` alias to point to `./components/*`:

```json title="src/admin/tsconfig.json"
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./components/*"]
    }
  }
}
```

Then, the `vite` alias configuration would be:

```ts title="medusa-config.ts"
import path from "path"

module.exports = defineConfig({
  // ...
  admin: {
    vite: () => ({
      resolve: {
        alias: {
          "@": path.resolve(__dirname, "./src/admin/components"),
        },
      },
    }),
  },
})
```


# Guide: Create Brand API Route

In the previous two chapters, you created a [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) that added the concepts of brands to your application, then created a [workflow to create a brand](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). In this chapter, you'll expose an API route that allows admin users to create a brand using the workflow from the previous chapter.

An API Route is an endpoint that acts as an entry point for other clients to interact with your Medusa customizations, such as the admin dashboard, storefronts, or third-party systems.

The Medusa core application provides a set of [admin](https://docs.medusajs.com/api/admin) and [store](https://docs.medusajs.com/api/store) API routes out-of-the-box. You can also create custom API routes to expose your custom functionalities.

### Prerequisites

- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md)

## 1. Create the API Route

You create an API route in a `route.{ts,js}` file under a sub-directory of the `src/api` directory. The file exports API Route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…).

Learn more about API routes [in this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md).

The route's path is the path of `route.{ts,js}` relative to `src/api`. So, to create the API route at `/admin/brands`, create the file `src/api/admin/brands/route.ts` with the following content:

![Directory structure of the Medusa application after adding the route](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869882/Medusa%20Book/brand-route-dir-overview-2_hjqlnf.jpg)

```ts title="src/api/admin/brands/route.ts"
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { 
  createBrandWorkflow,
} from "../../../workflows/create-brand"

type PostAdminCreateBrandType = {
  name: string
}

export const POST = async (
  req: MedusaRequest<PostAdminCreateBrandType>,
  res: MedusaResponse
) => {
  const { result } = await createBrandWorkflow(req.scope)
    .run({
      input: req.validatedBody,
    })

  res.json({ brand: result })
}
```

You export a route handler function with its name (`POST`) being the HTTP method of the API route you're exposing.

The function receives two parameters: a `MedusaRequest` object to access request details, and `MedusaResponse` object to return or manipulate the response. The `MedusaRequest` object's `scope` property is the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) that holds Framework tools and custom and core modules' services.

`MedusaRequest` accepts the request body's type as a type argument.

In the API route's handler, you execute the `createBrandWorkflow` by invoking it and passing the Medusa container `req.scope` as a parameter, then invoking its `run` method. You pass the workflow's input in the `input` property of the `run` method's parameter. You pass the request body's parameters using the `validatedBody` property of `MedusaRequest`.

You return a JSON response with the created brand using the `res.json` method.

***

## 2. Create Validation Schema

The API route you created accepts the brand's name in the request body. So, you'll create a schema used to validate incoming request body parameters.

Medusa uses [Zod](https://zod.dev/) to create validation schemas. These schemas are then used to validate incoming request bodies or query parameters.

Learn more about API route validation in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md).

You create a validation schema in a TypeScript or JavaScript file under a sub-directory of the `src/api` directory. So, create the file `src/api/admin/brands/validators.ts` with the following content:

![Directory structure of Medusa application after adding validators file](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869806/Medusa%20Book/brand-route-dir-overview-1_yfyjss.jpg)

```ts title="src/api/admin/brands/validators.ts"
import { z } from "zod"

export const PostAdminCreateBrand = z.object({
  name: z.string(),
})
```

You export a validation schema that expects in the request body an object having a `name` property whose value is a string.

You can then replace `PostAdminCreateBrandType` in `src/api/admin/brands/route.ts` with the following:

```ts title="src/api/admin/brands/route.ts"
// ...
import { z } from "zod"
import { PostAdminCreateBrand } from "./validators"

type PostAdminCreateBrandType = z.infer<typeof PostAdminCreateBrand>

// ...
```

***

## 3. Add Validation Middleware

A middleware is a function executed before the route handler when a request is sent to an API Route. It's useful to guard API routes, parse custom request body types, and apply validation on an API route.

Learn more about middlewares in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md).

Medusa provides a `validateAndTransformBody` middleware that accepts a Zod validation schema and returns a response error if a request is sent with body parameters that don't satisfy the validation schema.

Middlewares are defined in the special file `src/api/middlewares.ts`. So, to add the validation middleware on the API route you created in the previous step, create the file `src/api/middlewares.ts` with the following content:

![Directory structure of the Medusa application after adding the middleware](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869977/Medusa%20Book/brand-route-dir-overview-3_kcx511.jpg)

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { PostAdminCreateBrand } from "./admin/brands/validators"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/admin/brands",
      method: "POST",
      middlewares: [
        validateAndTransformBody(PostAdminCreateBrand),
      ],
    },
  ],
})
```

You define the middlewares using the `defineMiddlewares` function and export its returned value. The function accepts an object having a `routes` property, which is an array of middleware objects.

In the middleware object, you define three properties:

- `matcher`: a string or regular expression indicating the API route path to apply the middleware on. You pass the create brand's route `/admin/brands`.
- `method`: The HTTP method to restrict the middleware to, which is `POST`.
- `middlewares`: An array of middlewares to apply on the route. You pass the `validateAndTransformBody` middleware, passing it the Zod schema you created earlier.

The Medusa application will now validate the body parameters of `POST` requests sent to `/admin/brands` to ensure they match the Zod validation schema. If not, an error is returned in the response specifying the issues to fix in the request body.

***

## Test API Route

To test out the API route, start the Medusa application with the following command:

```bash npm2yarn
npm run dev
```

Since the `/admin/brands` API route has a `/admin` prefix, it's only accessible by authenticated admin users.

So, to retrieve an authenticated token of your admin user, send a `POST` request to the `/auth/user/emailpass` API Route:

```bash
curl -X POST 'http://localhost:9000/auth/user/emailpass' \
-H 'Content-Type: application/json' \
--data-raw '{
    "email": "admin@medusa-test.com",
    "password": "supersecret"
}'
```

Make sure to replace the email and password with your admin user's credentials.

Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md).

Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header:

```bash
curl -X POST 'http://localhost:9000/admin/brands' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data '{
    "name": "Acme"
}'
```

This returns the created brand in the response:

```json title="Example Response"
{
  "brand": {
    "id": "01J7AX9ES4X113HKY6C681KDZJ",
    "name": "Acme",
    "created_at": "2024-09-09T08:09:34.244Z",
    "updated_at": "2024-09-09T08:09:34.244Z"
  }
}
```

### Troubleshooting

#### Returned Empty Array

If you sent a request to the API route and received an empty array in the response, make sure you've correctly created the middleware at `src/api/middlewares.ts` (with correct spelling). This is a common mistake that can lead to the middleware not being applied, resulting in unexpected behavior.

***

## Summary

By following the previous example chapters, you implemented a custom feature that allows admin users to create a brand. You did that by:

1. Creating a module that defines and manages a `brand` table in the database.
2. Creating a workflow that uses the module's service to create a brand record, and implements the compensation logic to delete that brand in case an error occurs.
3. Creating an API route that allows admin users to create a brand.

***

## Next Steps: Associate Brand with Product

Now that you have brands in your Medusa application, you want to associate a brand with a product, which is defined in the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md).

In the next chapters, you'll learn how to build associations between data models defined in different modules.


# Guide: Implement Brand Module

In this chapter, you'll build a Brand Module that adds a `brand` table to the database and provides data-management features for it.

A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) is a reusable package of functionalities related to a single domain or integration. Medusa comes with multiple pre-built modules for core commerce needs, such as the [Cart Module](https://docs.medusajs.com/resources/commerce-modules/cart/index.html.md) that holds the data models and business logic for cart operations.

In a module, you create data models and business logic to manage them. In the next chapters, you'll see how you use the module to build commerce features.

![Diagram showcasing an overview of the Brand Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1746546820/Medusa%20Resources/brand-module_pg86gm.jpg)

## 1. Create Module Directory

Modules are created in a sub-directory of `src/modules`. So, start by creating the directory `src/modules/brand` that will hold the Brand Module's files.

![Directory structure in Medusa project after adding the brand directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868844/Medusa%20Book/brand-dir-overview-1_hxwvgx.jpg)

***

## 2. Create Data Model

A [data model](https://docs.medusajs.com/learn/fundamentals/modules#1-create-data-model/index.html.md) represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

You create a data model in a TypeScript or JavaScript file under the `models` directory of a module. So, to create a data model that represents a new `brand` table in the database, create the file `src/modules/brand/models/brand.ts` with the following content:

![Directory structure in module after adding the brand data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868920/Medusa%20Book/brand-dir-overview-2_lexhdl.jpg)

```ts title="src/modules/brand/models/brand.ts"
import { model } from "@medusajs/framework/utils"

export const Brand = model.define("brand", {
  id: model.id().primaryKey(),
  name: model.text(),
})
```

You create a `Brand` data model which has an `id` primary key property, and a `name` text property.

You define the data model using the `define` method of the DML. It accepts two parameters:

1. The first one is the name of the data model's table in the database. Use snake-case names.
2. The second is an object, which is the data model's schema.

Refer to the [Properties](https://docs.medusajs.com/learn/fundamentals/data-models/properties/index.html.md) chapter to learn more.

***

## 3. Create Module Service

You perform database operations on your data models in a [service](https://docs.medusajs.com/learn/fundamentals/modules#2-create-service/index.html.md), which is a class exported by the module and acts like an interface to its functionalities.

In this step, you'll create the Brand Module's service that provides methods to manage the `Brand` data model. In the next chapters, you'll use this service when exposing custom features that involve managing brands.

You define a service in a `service.ts` or `service.js` file at the root of your module's directory. So, create the file `src/modules/brand/service.ts` with the following content:

![Directory structure in module after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868984/Medusa%20Book/brand-dir-overview-3_jo7baj.jpg)

```ts title="src/modules/brand/service.ts" highlights={serviceHighlights}
import { MedusaService } from "@medusajs/framework/utils"
import { Brand } from "./models/brand"

class BrandModuleService extends MedusaService({
  Brand,
}) {

}

export default BrandModuleService
```

The `BrandModuleService` extends a class returned by `MedusaService` from the Modules SDK. This function generates a class with data-management methods for your module's data models.

The `MedusaService` function receives an object of the module's data models as a parameter, and generates methods to manage those data models. So, the `BrandModuleService` now has methods like `createBrands` and `retrieveBrand` to manage the `Brand` data model.

You'll use these methods in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md).

Refer to the [service factory reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) to learn more about the generated methods.

***

## 4. Export Module Definition

A module must export a definition that tells Medusa the name of the module and its main service. This definition is exported in an `index.ts` file at the module's root directory.

So, to export the Brand Module's definition, create the file `src/modules/brand/index.ts` with the following content:

![Directory structure in module after adding the definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869045/Medusa%20Book/brand-dir-overview-4_nf8ymw.jpg)

```ts title="src/modules/brand/index.ts"
import { Module } from "@medusajs/framework/utils"
import BrandModuleService from "./service"

export const BRAND_MODULE = "brand"

export default Module(BRAND_MODULE, {
  service: BrandModuleService,
})
```

You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name (`brand`). You'll use this name when you use this module in other customizations.
2. An object with a required property `service` indicating the module's main service.

You export `BRAND_MODULE` to reference the module's name more reliably in other customizations.

***

## 5. Add Module to Medusa's Configurations

To start using your module, you must add it to Medusa's configurations in `medusa-config.ts`.

The object passed to `defineConfig` in `medusa-config.ts` accepts a `modules` property, whose value is an array of modules to add to the application. So, add the following in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/brand",
    },
  ],
})
```

The Brand Module is now added to your Medusa application. You'll start using it in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md).

***

## 6. Generate and Run Migrations

A [migration](https://docs.medusajs.com/learn/fundamentals/modules#5-generate-migrations/index.html.md) is a TypeScript or JavaScript file that defines database changes made by a module. Migrations ensure that your module is re-usable and removes friction when working in a team, making it easy to reflect changes across team members' databases.

[Medusa's CLI tool](https://docs.medusajs.com/resources/medusa-cli/index.html.md) allows you to generate migration files for your module, then run those migrations to reflect the changes in the database. So, run the following commands in your Medusa application's directory:

```bash
npx medusa db:generate brand
npx medusa db:migrate
```

The `db:generate` command accepts as an argument the name of the module to generate the migrations for, and the `db:migrate` command runs all migrations that haven't been run yet in the Medusa application.

***

## Next Step: Create Brand Workflow

The Brand Module now creates a `brand` table in the database and provides a class to manage its records.

In the next chapter, you'll implement the functionality to create a brand in a workflow. You'll then use that workflow in a later chapter to expose an endpoint that allows admin users to create a brand.


# Build Custom Features

In the upcoming chapters, you'll follow step-by-step guides to build custom features in Medusa. These guides gradually introduce Medusa's concepts to help you understand what they are and how to use them.

By following these guides, you'll add brands to the Medusa application that you can associate with products.

To build a custom feature in Medusa, you need three main [Framework](https://docs.medusajs.com/learn/fundamentals/framework/index.html.md) tools:

- [Module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md): a package with commerce logic for a single domain. It defines new tables to add to the database, and a class of methods to manage these tables.
- [Workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md): a tool to perform an operation comprising multiple steps with built-in rollback and retry mechanisms.
- [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md): a REST endpoint that exposes commerce features to clients, such as the admin dashboard or a storefront. The API route executes a workflow that implements the commerce feature using modules.

![Diagram showcasing the flow of a custom developed feature](https://res.cloudinary.com/dza7lstvk/image/upload/v1725867628/Medusa%20Book/custom-development_nofvp6.jpg)

***

## Next Chapters: Brand Module Example

The next chapters will guide you to:

1. Build a Brand Module that creates a `Brand` data model and provides data-management features.
2. Add a workflow to create a brand.
3. Expose an API route that allows admin users to create a brand using the workflow.


# Guide: Create Brand Workflow

This chapter builds on the work from the [previous chapter](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) where you created a Brand Module.

After adding custom modules to your application, you build commerce features around them using workflows. A workflow is a series of queries and actions, called steps, that complete a task spanning across modules. You construct a workflow similar to a regular function, but it's a special function that allows you to define roll-back logic, retry configurations, and more advanced features.

The workflow you'll create in this chapter will use the Brand Module's service to implement the feature of creating a brand. In the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll expose an API route that allows admin users to create a brand, and you'll use this workflow in the route's implementation.

Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md).

### Prerequisites

- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)

***

## 1. Create createBrandStep

A workflow consists of a series of steps, each step created in a TypeScript or JavaScript file under the `src/workflows` directory. A step is defined using `createStep` from the Workflows SDK

The workflow you're creating in this guide has one step to create the brand. So, create the file `src/workflows/create-brand.ts` with the following content:

![Directory structure in the Medusa project after adding the file for createBrandStep](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869184/Medusa%20Book/brand-workflow-dir-overview-1_fjvf5j.jpg)

```ts title="src/workflows/create-brand.ts"
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { BRAND_MODULE } from "../modules/brand"
import BrandModuleService from "../modules/brand/service"

export type CreateBrandStepInput = {
  name: string
}

export const createBrandStep = createStep(
  "create-brand-step",
  async (input: CreateBrandStepInput, { container }) => {
    const brandModuleService: BrandModuleService = container.resolve(
      BRAND_MODULE
    )

    const brand = await brandModuleService.createBrands(input)

    return new StepResponse(brand, brand.id)
  }
)
```

You create a `createBrandStep` using the `createStep` function. It accepts the step's unique name as a first parameter, and the step's function as a second parameter.

The step function receives two parameters: input passed to the step when it's invoked, and an object of general context and configurations. This object has a `container` property, which is the Medusa container.

The [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) is a registry of Framework and commerce tools accessible in your customizations, such as a workflow's step. The Medusa application registers the services of core and custom modules in the container, allowing you to resolve and use them.

So, In the step function, you use the Medusa container to resolve the Brand Module's service and use its generated `createBrands` method, which accepts an object of brands to create.

Learn more about the generated `create` method's usage in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/create/index.html.md).

A step must return an instance of `StepResponse`. Its first parameter is the data returned by the step, and the second is the data passed to the compensation function, which you'll learn about next.

### Add Compensation Function to Step

You define for each step a compensation function that's executed when an error occurs in the workflow. The compensation function defines the logic to roll-back the changes made by the step. This ensures your data remains consistent if an error occurs, which is especially useful when you integrate third-party services.

Learn more about the compensation function in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md).

To add a compensation function to the `createBrandStep`, pass it as a third parameter to `createStep`:

```ts title="src/workflows/create-brand.ts"
export const createBrandStep = createStep(
  // ...
  async (id: string, { container }) => {
    const brandModuleService: BrandModuleService = container.resolve(
      BRAND_MODULE
    )

    await brandModuleService.deleteBrands(id)
  }
)
```

The compensation function's first parameter is the brand's ID which you passed as a second parameter to the step function's returned `StepResponse`. It also accepts a context object with a `container` property as a second parameter, similar to the step function.

In the compensation function, you resolve the Brand Module's service from the Medusa container, then use its generated `deleteBrands` method to delete the brand created by the step. This method accepts the ID of the brand to delete.

Learn more about the generated `delete` method's usage in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/delete/index.html.md).

So, if an error occurs during the workflow's execution, the brand that was created by the step is deleted to maintain data consistency.

***

## 2. Create createBrandWorkflow

You can now create the workflow that runs the `createBrandStep`. A workflow is created in a TypeScript or JavaScript file under the `src/workflows` directory. In the file, you use `createWorkflow` from the Workflows SDK to create the workflow.

Add the following content in the same `src/workflows/create-brand.ts` file:

```ts title="src/workflows/create-brand.ts"
// other imports...
import {
  // ...
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

// ...

type CreateBrandWorkflowInput = {
  name: string
}

export const createBrandWorkflow = createWorkflow(
  "create-brand",
  (input: CreateBrandWorkflowInput) => {
    const brand = createBrandStep(input)

    return new WorkflowResponse(brand)
  }
)
```

You create the `createBrandWorkflow` using the `createWorkflow` function. This function accepts two parameters: the workflow's unique name, and the workflow's constructor function holding the workflow's implementation.

The constructor function accepts the workflow's input as a parameter. In the function, you invoke the `createBrandStep` you created in the previous step to create a brand.

A workflow must return an instance of `WorkflowResponse`. It accepts as a parameter the data to return to the workflow's executor.

***

## Next Steps: Expose Create Brand API Route

You now have a `createBrandWorkflow` that you can execute to create a brand.

In the next chapter, you'll add an API route that allows admin users to create a brand. You'll learn how to create the API route, and execute in it the workflow you implemented in this chapter.


# Customize Medusa Admin Dashboard

In the previous chapters, you've customized your Medusa application to [add brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md), [expose an API route to create brands](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), and [linked brands to products](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md).

After customizing and extending your application with new features, you may need to provide an interface for admin users to utilize these features. The Medusa Admin dashboard is extendable, allowing you to:

- Insert components, called [widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md), into existing pages. For example, you can add a widget to the product details page that shows the product's brand.
- Add new pages, called [UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md). For example, you can create a new page in the dashboard that lists all brands in the store.

Within these customizations, you can send requests to custom API routes, allowing admin users to view and manage custom resources on the dashboard.

***

## Next Chapters: View Brands in Dashboard

In the next chapters, you'll continue with the brands example to:

- Add a new section to the product details page that shows the product's brand.
- Add a new page in the dashboard that shows all brands in the store.


# Create Brands UI Route in Admin

In this chapter, you'll add a UI route to the admin dashboard that shows all [brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) in a new page. You'll retrieve the brands from the server and display them in a table with pagination.

### Prerequisites

- [Brands Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)

## 1. Get Brands API Route

In a [previous chapter](https://docs.medusajs.com/learn/customization/extend-features/query-linked-records/index.html.md), you learned how to add an API route that retrieves brands and their products using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). You'll expand that API route to support pagination, so that on the admin dashboard you can show the brands in a paginated table.

Replace or create the `GET` API route at `src/api/admin/brands/route.ts` with the following:

```ts title="src/api/admin/brands/route.ts" highlights={apiRouteHighlights}
// other imports...
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve("query")
  
  const { 
    data: brands, 
    metadata: { count, take, skip } = {},
  } = await query.graph({
    entity: "brand",
    ...req.queryConfig,
  })

  res.json({ 
    brands,
    count,
    limit: take,
    offset: skip,
  })
}
```

In the API route, you use Query's `graph` method to retrieve the brands. In the method's object parameter, you spread the `queryConfig` property of the request object. This property holds configurations for pagination and retrieved fields.

The query configurations are combined from default configurations, which you'll add next, and the request's query parameters:

- `fields`: The fields to retrieve in the brands.
- `limit`: The maximum number of items to retrieve.
- `offset`: The number of items to skip before retrieving the returned items.

When you pass pagination configurations to the `graph` method, the returned object has the pagination's details in a `metadata` property, whose value is an object having the following properties:

- `count`: The total count of items.
- `take`: The maximum number of items returned in the `data` array.
- `skip`: The number of items skipped before retrieving the returned items.

You return in the response the retrieved brands and the pagination configurations.

Refer to the [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-pagination/index.html.md) chapter to learn more about applying pagination when retrieving linked records.

***

## 2. Add Default Query Configurations

Next, you'll set the default query configurations of the above API route and allow passing query parameters to change the configurations.

Medusa provides a `validateAndTransformQuery` middleware that validates the accepted query parameters for a request and sets the default Query configuration. So, in `src/api/middlewares.ts`, add a new middleware configuration object:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  validateAndTransformQuery,
} from "@medusajs/framework/http"
import { createFindParams } from "@medusajs/medusa/api/utils/validators"
// other imports...

export const GetBrandsSchema = createFindParams()

export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/brands",
      method: "GET",
      middlewares: [
        validateAndTransformQuery(
          GetBrandsSchema,
          {
            defaults: [
              "id",
              "name",
              "products.*",
            ],
            isList: true,
          }
        ),
      ],
    },

  ],
})
```

You apply the `validateAndTransformQuery` middleware on the `GET /admin/brands` API route. The middleware accepts two parameters:

- A [Zod](https://zod.dev/) schema that a request's query parameters must satisfy. Medusa provides `createFindParams` that generates a Zod schema with the following properties:
  - `fields`: A comma-separated string indicating the fields to retrieve.
  - `limit`: The maximum number of items to retrieve.
  - `offset`: The number of items to skip before retrieving the returned items.
  - `order`: The name of the field to sort the items by. Learn more about sorting in [the API reference](https://docs.medusajs.com/api/admin#sort-order)
- An object of Query configurations having the following properties:
  - `defaults`: An array of default fields and relations to retrieve.
  - `isList`: Whether the API route returns a list of items.

By applying the above middleware, you can pass pagination configurations to `GET /admin/brands`, which will return a paginated list of brands. You'll see how it works when you create the UI route.

Refer to the [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query#request-query-configurations/index.html.md) chapter to learn more about using the `validateAndTransformQuery` middleware to configure Query.

***

## 3. Initialize JS SDK

In your custom UI route, you'll retrieve the brands by sending a request to the Medusa server. Medusa has a [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) that simplifies sending requests to the core API route.

If you didn't follow the [previous chapter](https://docs.medusajs.com/learn/customization/customize-admin/widget/index.html.md), create the file `src/admin/lib/sdk.ts` with the following content:

![The directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414606/Medusa%20Book/brands-admin-dir-overview-1_jleg0t.jpg)

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

You initialize the SDK passing it the following options:

- `baseUrl`: The URL to the Medusa server.
- `debug`: Whether to enable logging debug messages. This should only be enabled in development.
- `auth.type`: The authentication method used in the client application, which is `session` in the Medusa Admin dashboard.

Notice that you use `import.meta.env` to access environment variables in your customizations because the Medusa Admin is built on top of Vite. Learn more in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md).

You can now use the SDK to send requests to the Medusa server.

Refer to the [JS SDK Reference](https://docs.medusajs.com/resources/js-sdk/index.html.md) to learn more about the it, its options, and how to use it to send requests to Medusa's API routes.

***

## 4. Add a UI Route to Show Brands

You'll now add the UI route that shows the paginated list of brands. A UI route is a React component created in a `page.tsx` file under a sub-directory of `src/admin/routes`. The file's path relative to src/admin/routes determines its path in the dashboard.

Refer to the [UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md) chapter to learn more about UI routes.

So, to add the UI route at the `localhost:9000/app/brands` path, create the file `src/admin/routes/brands/page.tsx` with the following content:

![Directory structure of the Medusa application after adding the UI route.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733472011/Medusa%20Book/brands-admin-dir-overview-3_syytld.jpg)

```tsx title="src/admin/routes/brands/page.tsx" highlights={uiRouteHighlights}
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { TagSolid } from "@medusajs/icons"
import { 
  Container,
} from "@medusajs/ui"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../../lib/sdk"
import { useMemo, useState } from "react"

const BrandsPage = () => {
  // TODO retrieve brands

  return (
    <Container className="divide-y p-0">
      {/* TODO show brands */}
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Brands",
  icon: TagSolid,
})

export default BrandsPage
```

A route's file must export the React component that will be rendered in the new page. It must be the default export of the file. You can also export configurations that add a link in the sidebar for the UI route. You create these configurations using `defineRouteConfig` from the Admin Extension SDK.

So far, you only show a container. In admin customizations, use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md) to maintain a consistent user interface and design in the dashboard.

### Retrieve Brands From API Route

You'll now update the UI route to retrieve the brands from the API route you added earlier.

First, add the following type in `src/admin/routes/brands/page.tsx`:

```tsx title="src/admin/routes/brands/page.tsx"
type Brand = {
  id: string
  name: string
}
type BrandsResponse = {
  brands: Brand[]
  count: number
  limit: number
  offset: number
}
```

You define the type for a brand, and the type of expected response from the `GET /admin/brands` API route.

To display the brands, you'll use Medusa UI's [DataTable](https://docs.medusajs.com/ui/components/data-table/index.html.md) component. So, add the following imports in `src/admin/routes/brands/page.tsx`:

```tsx title="src/admin/routes/brands/page.tsx"
import { 
  // ...
  Heading,
  createDataTableColumnHelper,
  DataTable,
  DataTablePaginationState,
  useDataTable,
} from "@medusajs/ui"
```

You import the `DataTable` component and the following utilities:

- `createDataTableColumnHelper`: A utility to create columns for the data table.
- `DataTablePaginationState`: A type that holds the pagination state of the data table.
- `useDataTable`: A hook to initialize and configure the data table.

You also import the `Heading` component to show a heading above the data table.

Next, you'll define the table's columns. Add the following before the `BrandsPage` component:

```tsx title="src/admin/routes/brands/page.tsx"
const columnHelper = createDataTableColumnHelper<Brand>()

const columns = [
  columnHelper.accessor("id", {
    header: "ID",
  }),
  columnHelper.accessor("name", {
    header: "Name",
  }),
]
```

You use the `createDataTableColumnHelper` utility to create columns for the data table. You define two columns for the ID and name of the brands.

Then, replace the `// TODO retrieve brands` in the component with the following:

```tsx title="src/admin/routes/brands/page.tsx" highlights={queryHighlights}
const limit = 15
const [pagination, setPagination] = useState<DataTablePaginationState>({
  pageSize: limit,
  pageIndex: 0,
})
const offset = useMemo(() => {
  return pagination.pageIndex * limit
}, [pagination])

const { data, isLoading } = useQuery<BrandsResponse>({
  queryFn: () => sdk.client.fetch(`/admin/brands`, {
    query: {
      limit,
      offset,
    },
  }),
  queryKey: [["brands", limit, offset]],
})

// TODO configure data table
```

To enable pagination in the `DataTable` component, you need to define a state variable of type `DataTablePaginationState`. It's an object having the following properties:

- `pageSize`: The maximum number of items per page. You set it to `15`.
- `pageIndex`: A zero-based index of the current page of items.

You also define a memoized `offset` value that indicates the number of items to skip before retrieving the current page's items.

Then, you use `useQuery` from [Tanstack (React) Query](https://tanstack.com/query/latest) to query the Medusa server. Tanstack Query provides features like asynchronous state management and optimized caching.

Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency.

In the `queryFn` function that executes the query, you use the JS SDK's `client.fetch` method to send a request to your custom API route. The first parameter is the route's path, and the second is an object of request configuration and data. You pass the query parameters in the `query` property.

This sends a request to the [Get Brands API route](#1-get-brands-api-route), passing the pagination query parameters. Whenever `currentPage` is updated, the `offset` is also updated, which will send a new request to retrieve the brands for the current page.

### Display Brands Table

Finally, you'll display the brands in a  data table. Replace the `// TODO configure data table` in the component with the following:

```tsx title="src/admin/routes/brands/page.tsx"
const table = useDataTable({
  columns,
  data: data?.brands || [],
  getRowId: (row) => row.id,
  rowCount: data?.count || 0,
  isLoading,
  pagination: {
    state: pagination,
    onPaginationChange: setPagination,
  },
})
```

You use the `useDataTable` hook to initialize and configure the data table. It accepts an object with the following properties:

- `columns`: The columns of the data table. You created them using the `createDataTableColumnHelper` utility.
- `data`: The brands to display in the table.
- `getRowId`: A function that returns a unique identifier for a row.
- `rowCount`: The total count of items. This is used to determine the number of pages.
- `isLoading`: A boolean indicating whether the data is loading.
- `pagination`: An object to configure pagination. It accepts the following properties:
  - `state`: The pagination state of the data table.
  - `onPaginationChange`: A function to update the pagination state.

Then, replace the `{/* TODO show brands */}` in the return statement with the following:

```tsx title="src/admin/routes/brands/page.tsx"
<DataTable instance={table}>
  <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
    <Heading>Brands</Heading>
  </DataTable.Toolbar>
  <DataTable.Table />
  <DataTable.Pagination />
</DataTable>
```

This renders the data table that shows the brands with pagination. The `DataTable` component accepts the `instance` prop, which is the object returned by the `useDataTable` hook.

***

## Test it Out

To test out the UI route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the admin dashboard at `http://localhost:9000/app`. After you log in, you'll find a new "Brands" sidebar item. Click on it to see the brands in your store. You can also go to `http://localhost:9000/app/brands` to see the page.

![A new sidebar item is added for the new brands UI route. The UI route shows the table of brands with pagination.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733421074/Medusa%20Book/Screenshot_2024-12-05_at_7.46.52_PM_slcdqd.png)

***

## Summary

By following the previous chapters, you:

- Injected a widget into the product details page to show the product's brand.
- Created a UI route in the Medusa Admin that shows the list of brands.

***

## Next Steps: Integrate Third-Party Systems

Your customizations often span across systems, where you need to retrieve data or perform operations in a third-party system.

In the next chapters, you'll learn about the concepts that facilitate integrating third-party systems in your application. You'll integrate a dummy third-party system and sync the brands between it and the Medusa application.


# Guide: Add Product's Brand Widget in Admin

In this chapter, you'll customize the product details page of the Medusa Admin dashboard to show the product's [brand](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md). You'll create a widget that is injected into a pre-defined zone in the page, and in the widget you'll retrieve the product's brand from the server and display it.

### Prerequisites

- [Brands linked to products](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md)

## 1. Initialize JS SDK

In your custom widget, you'll retrieve the product's brand by sending a request to the Medusa server. Medusa has a [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) that simplifies sending requests to the server's API routes.

So, you'll start by configuring the JS SDK. Create the file `src/admin/lib/sdk.ts` with the following content:

![The directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414606/Medusa%20Book/brands-admin-dir-overview-1_jleg0t.jpg)

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

You initialize the SDK passing it the following options:

- `baseUrl`: The URL to the Medusa server.
- `debug`: Whether to enable logging debug messages. This should only be enabled in development.
- `auth.type`: The authentication method used in the client application, which is `session` in the Medusa Admin dashboard.

Notice that you use `import.meta.env` to access environment variables in your customizations because the Medusa Admin is built on top of Vite. Learn more in the [Admin Environment Variables](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md) chapter.

You can now use the SDK to send requests to the Medusa server.

Refer to the [JS SDK Reference](https://docs.medusajs.com/resources/js-sdk/index.html.md) to learn more about the it, its options, and how to use it to send requests to Medusa's API routes.

***

## 2. Add Widget to Product Details Page

You'll now add a widget to the product-details page. A [widget](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md) is a React component that's injected into pre-defined zones in the Medusa Admin dashboard. It's created in a `.tsx` file under the `src/admin/widgets` directory.

To create a widget that shows a product's brand in its details page, create the file `src/admin/widgets/product-brand.tsx` with the following content:

![Directory structure of the Medusa application after adding the widget](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414684/Medusa%20Book/brands-admin-dir-overview-2_eq5xhi.jpg)

```tsx title="src/admin/widgets/product-brand.tsx" highlights={highlights}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { DetailWidgetProps, AdminProduct } from "@medusajs/framework/types"
import { clx, Container, Heading, Text } from "@medusajs/ui"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"

type AdminProductBrand = AdminProduct & {
  brand?: {
    id: string
    name: string
  }
}

const ProductBrandWidget = ({ 
  data: product,
}: DetailWidgetProps<AdminProduct>) => {
  const { data: queryResult } = useQuery({
    queryFn: () => sdk.admin.product.retrieve(product.id, {
      fields: "+brand.*",
    }),
    queryKey: [["product", product.id]],
  })
  const brandName = (queryResult?.product as AdminProductBrand)?.brand?.name

  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <div>
          <Heading level="h2">Brand</Heading>
        </div>
      </div>
      <div
        className={clx(
          `text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4`
        )}
      >
        <Text size="small" weight="plus" leading="compact">
          Name
        </Text>

        <Text
          size="small"
          leading="compact"
          className="whitespace-pre-line text-pretty"
        >
          {brandName || "-"}
        </Text>
      </div>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductBrandWidget
```

A widget's file must export:

- A React component to be rendered in the specified injection zone. The component must be the file's default export.
- A configuration object created with `defineWidgetConfig` from the Admin Extension SDK. The function receives an object as a parameter that has a `zone` property, whose value is the zone to inject the widget to.

Since the widget is injected at the top of the product details page, the widget receives the product's details as a parameter.

In the widget, you use [Tanstack (React) Query](https://tanstack.com/query/latest) to query the Medusa server. Tanstack Query provides features like asynchronous state management and optimized caching. In the `queryFn` function that executes the query, you use the JS SDK to send a request to the [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid), passing `+brand.*` in the `fields` query parameter to retrieve the product's brand.

Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency.

You then render a section that shows the brand's name. In admin customizations, use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md) to maintain a consistent user interface and design in the dashboard.

***

## Test it Out

To test out your widget, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the admin dashboard at `http://localhost:9000/app`. After you log in, open the page of a product that has a brand. You'll see a new section at the top showing the brand's name.

![The widget is added as the first section of the product details page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414415/Medusa%20Book/Screenshot_2024-12-05_at_5.59.25_PM_y85m14.png)

***

## Admin Components Guides

When building your widget, you may need more complicated components. For example, you may add a form to the above widget to set the product's brand.

The [Admin Components guides](https://docs.medusajs.com/resources/admin-components/index.html.md) show you how to build and use common components in the Medusa Admin, such as forms, tables, JSON data viewer, and more. The components in the guides also follow the Medusa Admin's design convention.

***

## Next Chapter: Add UI Route for Brands

In the next chapter, you'll add a UI route that displays the list of brands in your application and allows admin users.


# Guide: Define Module Link Between Brand and Product

In this chapter, you'll learn how to define a module link between a brand defined in the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md), and a product defined in the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) that's available in your Medusa application out-of-the-box.

Modules are [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md) from other resources, ensuring that they're integrated into the Medusa application without side effects. However, you may need to build relations between data models of different modules, or you're trying to extend data models from [Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md) with custom properties. To do that, you define module links.

A [module link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) forms an association between two data models of different modules while maintaining module isolation. You can then manage and query linked records of the data models using Medusa's Modules SDK.

In this chapter, you'll define a module link between the `Brand` data model of the Brand Module, and the `Product` data model of the Product Module. In later chapters, you'll manage and retrieve linked product and brand records.

### Prerequisites

- [Brand Module having a Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)

## 1. Define Link

Links are defined in a TypeScript or JavaScript file under the `src/links` directory. The file defines and exports the link using `defineLink` from the Modules SDK.

So, to define a link between the `Product` and `Brand` models, create the file `src/links/product-brand.ts` with the following content:

![The directory structure of the Medusa application after adding the link.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733329897/Medusa%20Book/brands-link-dir-overview_t1rhlp.jpg)

```ts title="src/links/product-brand.ts" highlights={highlights}
import BrandModule from "../modules/brand"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    isList: true,
  },
  BrandModule.linkable.brand
)
```

You import each module's definition object from the `index.ts` file of the module's directory. Each module object has a special `linkable` property that holds the data models' link configurations.

The `defineLink` function accepts two parameters of the same type, which is either:

- The data model's link configuration, which you access from the Module's `linkable` property;
- Or an object that has two properties:
  - `linkable`: the data model's link configuration, which you access from the Module's `linkable` property.
  - `isList`: A boolean indicating whether many records of the data model can be linked to the other model.

So, in the above code snippet, you define a link between the `Product` and `Brand` data models. Since a brand can be associated with multiple products, you enable `isList` in the `Product` model's object.

***

## 2. Sync the Link to the Database

A module link is represented in the database as a table that stores the IDs of linked records. So, after defining the link, run the following command to create the module link's table in the database:

```bash
npx medusa db:migrate
```

This command reflects migrations on the database and syncs module links, which creates a table for the `product-brand` link.

You can also run the `npx medusa db:sync-links` to just sync module links without running migrations.

***

## Next Steps: Extend Create Product Flow

In the next chapter, you'll extend Medusa's workflow and API route that create a product to allow associating a brand with a product. You'll also learn how to link brand and product records.


# Guide: Extend Create Product Flow

After linking the [custom Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) in the [previous chapter](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md), you'll extend the create product workflow and API route to allow associating a brand with a product.

Some API routes, including the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts), accept an `additional_data` request body parameter. This parameter can hold custom data that's passed to the [hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) of the workflow executed in the API route, allowing you to consume those hooks and perform actions with the custom data.

So, in this chapter, to extend the create product flow and associate a brand with a product, you will:

- Consume the [productsCreated](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow#productsCreated/index.html.md) hook of the [createProductsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md), which is executed within the workflow after the product is created. You'll link the product with the brand passed in the `additional_data` parameter.
- Extend the Create Product API route to allow passing a brand ID in `additional_data`.

To learn more about the `additional_data` property and the API routes that accept additional data, refer to the [Additional Data](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md) chapter.

### Prerequisites

- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)
- [Defined link between the Brand and Product data models.](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md)

***

## 1. Consume the productsCreated Hook

A [workflow hook](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) is a point in a workflow where you can inject a step to perform a custom functionality. Consuming a workflow hook allows you to extend the features of a workflow and, consequently, the API route that uses it.

The [createProductsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md) used in the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts) has a `productsCreated` hook that runs after the product is created. You'll consume this hook to link the created product with the brand specified in the request parameters.

To consume the `productsCreated` hook, create the file `src/workflows/hooks/created-product.ts` with the following content:

![Directory structure after creating the hook's file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733384338/Medusa%20Book/brands-hook-dir-overview_ltwr5h.jpg)

```ts title="src/workflows/hooks/created-product.ts" highlights={hook1Highlights}
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
import { StepResponse } from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
import { LinkDefinition } from "@medusajs/framework/types"
import { BRAND_MODULE } from "../../modules/brand"
import BrandModuleService from "../../modules/brand/service"

createProductsWorkflow.hooks.productsCreated(
  (async ({ products, additional_data }, { container }) => {
    if (!additional_data?.brand_id) {
      return new StepResponse([], [])
    }

    const brandModuleService: BrandModuleService = container.resolve(
      BRAND_MODULE
    )
    // if the brand doesn't exist, an error is thrown.
    await brandModuleService.retrieveBrand(additional_data.brand_id as string)

    // TODO link brand to product
  })
)
```

Workflows have a special `hooks` property to access its hooks and consume them. Each hook, such as `productsCreated`, accepts a step function as a parameter. The step function accepts the following parameters:

1. An object having an `additional_data` property, which is the custom data passed in the request body under `additional_data`. The object will also have properties passed from the workflow to the hook, which in this case is the `products` property that holds an array of the created products.
2. An object of properties related to the step's context. It has a `container` property whose value is the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) to resolve Framework and commerce tools.

In the step, if a brand ID is passed in `additional_data`, you resolve the Brand Module's service and use its generated `retrieveBrand` method to retrieve the brand by its ID. The `retrieveBrand` method will throw an error if the brand doesn't exist.

### Link Brand to Product

Next, you want to create a link between the created products and the brand. To do so, you use [Link](https://docs.medusajs.com/learn/fundamentals/module-links/link/index.html.md), which is a class from the Modules SDK that provides methods to manage linked records.

To use Link in the `productsCreated` hook, replace the `TODO` with the following:

```ts title="src/workflows/hooks/created-product.ts" highlights={hook2Highlights}
const link = container.resolve("link")
const logger = container.resolve("logger")

const links: LinkDefinition[] = []

for (const product of products) {
  links.push({
    [Modules.PRODUCT]: {
      product_id: product.id,
    },
    [BRAND_MODULE]: {
      brand_id: additional_data.brand_id,
    },
  })
}

await link.create(links)

logger.info("Linked brand to products")

return new StepResponse(links, links)
```

You resolve Link from the container. Then, you loop over the created products to assemble an array of links to be created. After that, you pass the array of links to Link's `create` method, which will link the product and brand records.

Each property in the link object is the name of a module, and its value is an object having a `{model_name}_id` property, where `{model_name}` is the snake-case name of the module's data model. Its value is the ID of the record to be linked. The link object's properties must be set in the same order as the link configurations passed to `defineLink`.

![Diagram showcasing how the order of defining a link affects creating the link](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386156/Medusa%20Book/remote-link-brand-product-exp_fhjmg4.jpg)

Finally, you return an instance of `StepResponse` returning the created links.

### Dismiss Links in Compensation

You can pass as a second parameter of the hook a compensation function that undoes what the step did. It receives as a first parameter the returned `StepResponse`'s second parameter, and the step context object as a second parameter.

To undo creating the links in the hook, pass the following compensation function as a second parameter to `productsCreated`:

```ts title="src/workflows/hooks/created-product.ts"
createProductsWorkflow.hooks.productsCreated(
  // ...
  (async (links, { container }) => {
    if (!links?.length) {
      return
    }

    const link = container.resolve("link")

    await link.dismiss(links)
  })
)
```

In the compensation function, if the `links` parameter isn't empty, you resolve Link from the container and use its `dismiss` method. This method removes a link between two records. It accepts the same parameter as the `create` method.

***

## 2. Configure Additional Data Validation

Now that you've consumed the `productsCreated` hook, you want to configure the `/admin/products` API route that creates a new product to accept a brand ID in its `additional_data` parameter.

You configure the properties accepted in `additional_data` in the `src/api/middlewares.ts` that exports middleware configurations. So, create the file (or, if already existing, add to the file) `src/api/middlewares.ts` the following content:

![Directory structure after adding the middelwares file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386868/Medusa%20Book/brands-middleware-dir-overview_uczos1.jpg)

```ts title="src/api/middlewares.ts"
import { defineMiddlewares } from "@medusajs/framework/http"
import { z } from "zod"

// ...

export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/products",
      method: ["POST"],
      additionalDataValidator: {
        brand_id: z.string().optional(),
      },
    },
  ],
})
```

Objects in `routes` accept an `additionalDataValidator` property that configures the validation rules for custom properties passed in the `additional_data` request parameter. It accepts an object whose keys are custom property names, and their values are validation rules created using [Zod](https://zod.dev/).

So, `POST` requests sent to `/admin/products` can now pass the ID of a brand in the `brand_id` property of `additional_data`.

***

## Test it Out

To test it out, first, retrieve the authentication token of your admin user by sending a `POST` request to `/auth/user/emailpass`:

```bash
curl -X POST 'http://localhost:9000/auth/user/emailpass' \
-H 'Content-Type: application/json' \
--data-raw '{
    "email": "admin@medusa-test.com",
    "password": "supersecret"
}'
```

Make sure to replace the email and password in the request body with your user's credentials.

Then, send a `POST` request to `/admin/products` to create a product, and pass in the `additional_data` parameter a brand's ID:

```bash
curl -X POST 'http://localhost:9000/admin/products' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data '{
    "title": "Product 1",
    "options": [
      {
        "title": "Default option",
        "values": ["Default option value"]
      }
    ],
    "shipping_profile_id": "{shipping_profile_id}",
    "additional_data": {
        "brand_id": "{brand_id}"
    }
}'
```

Make sure to replace `{token}` with the token you received from the previous request, `shipping_profile_id` with the ID of a shipping profile in your application, and `{brand_id}` with the ID of a brand in your application. You can retrieve the ID of a shipping profile either from the Medusa Admin, or the [List Shipping Profiles API route](https://docs.medusajs.com/api/admin#shipping-profiles_getshippingprofiles).

The request creates a product and returns it.

In the Medusa application's logs, you'll find the message `Linked brand to products`, indicating that the workflow hook handler ran and linked the brand to the products.

***

## Next Steps: Query Linked Brands and Products

Now that you've extending the create-product flow to link a brand to it, you want to retrieve the brand details of a product. You'll learn how to do so in the next chapter.


# Extend Core Commerce Features

In the upcoming chapters, you'll learn about the concepts and tools to extend Medusa's core commerce features.

In other commerce platforms, you extend core features and models through hacky workarounds that can introduce unexpected issues and side effects across the platform. It also makes your application difficult to maintain and upgrade in the long run.

The Medusa Framework and orchestration tools mitigate these issues while supporting all your customization needs:

- [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md): Link data models of different modules without building direct dependencies, ensuring that the Medusa application integrates your modules without side effects.
- [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md): inject custom functionalities into a workflow at predefined points, called hooks. This allows you to perform custom actions as a part of a core workflow without hacky workarounds.
- [Additional Data in API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md): Configure core API routes to accept request parameters relevant to your customizations. These parameters are passed to the underlying workflow's hooks, where you can manage your custom data as part of an existing flow.

***

## Next Chapters: Link Brands to Products Example

The next chapters explain how to use the tools mentioned above with step-by-step guides. You'll continue with the [brands example from the previous chapters](https://docs.medusajs.com/learn/customization/custom-features/index.html.md) to:

- Link brands from the custom [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) to products from Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md).
- Extend the core product-creation workflow and the API route that uses it to allow setting the brand of a newly created product.
- Retrieve a product's associated brand's details.


# Guide: Query Product's Brands

In the previous chapters, you [defined a link](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) between the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md), then [extended the create-product flow](https://docs.medusajs.com/learn/customization/extend-features/extend-create-product/index.html.md) to link a product to a brand.

In this chapter, you'll learn how to retrieve a product's brand (and vice-versa) in two ways: Using Medusa's existing API route, or in customizations, such as a custom API route.

### Prerequisites

- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)
- [Defined link between the Brand and Product data models.](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md)

***

## Approach 1: Retrieve Brands in Existing API Routes

Medusa's existing API routes accept a `fields` query parameter that allows you to specify the fields and relations of a model to retrieve. So, when you send a request to the [List Products](https://docs.medusajs.com/api/admin#products_getproducts), [Get Product](https://docs.medusajs.com/api/admin#products_getproductsid), or any product-related store or admin routes that accept a `fields` query parameter, you can specify in this parameter to return the product's brands.

Learn more about using the `fields` query parameter to retrieve custom linked data models in the [Retrieve Custom Linked Data Models from Medusa's API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links/index.html.md) chapter.

For example, send the following request to retrieve the list of products with their brands:

```bash
curl 'http://localhost:9000/admin/products?fields=+brand.*' \
-H 'Authorization: Bearer {token}'
```

Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication).

Any product that is linked to a brand will have a `brand` property in its object:

```json title="Example Product Object"
{
  "id": "prod_123",
  // ...
  "brand": {
    "id": "01JEB44M61BRM3ARM2RRMK7GJF",
    "name": "Acme",
    "created_at": "2024-12-05T09:59:08.737Z",
    "updated_at": "2024-12-05T09:59:08.737Z",
    "deleted_at": null
  }
}
```

By using the `fields` query parameter, you don't have to re-create existing API routes to get custom data models that you linked to core data models.

### Limitations: Filtering by Brands in Existing API Routes

While you can retrieve linked records using the `fields` query parameter of an existing API route, you can't filter by linked records.

Instead, you'll have to create a custom API route that uses [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md) to retrieve linked records with filters.

***

## Approach 2: Use Query to Retrieve Linked Records

You can also retrieve linked records using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). Query allows you to retrieve data across modules with filters, pagination, and more. You can resolve Query from the Medusa container and use it in your API route or workflow.

For example, you can create an API route that retrieves brands and their products. If you followed the [Create Brands API route chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll have the file `src/api/admin/brands/route.ts` with a `POST` API route. Add a new `GET` function to the same file:

```ts title="src/api/admin/brands/route.ts" highlights={highlights}
// other imports...
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve("query")
  
  const { data: brands } = await query.graph({
    entity: "brand",
    fields: ["*", "products.*"],
  })

  res.json({ brands })
}
```

This adds a `GET` API route at `/admin/brands`. In the API route, you resolve Query from the Medusa container. Query has a `graph` method that runs a query to retrieve data. It accepts an object having the following properties:

- `entity`: The data model's name as specified in the first parameter of `model.define`.
- `fields`: An array of properties and relations to retrieve. You can pass:
  - A property's name, such as `id`, or `*` for all properties.
  - A relation or linked model's name, such as `products` (use the plural name since brands are linked to list of products). You suffix the name with `.*` to retrieve all its properties.

`graph` returns an object having a `data` property, which is the retrieved brands. You return the brands in the response.

### Test it Out

To test the API route out, send a `GET` request to `/admin/brands`:

```bash
curl 'http://localhost:9000/admin/brands' \
-H 'Authorization: Bearer {token}'
```

Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication).

This returns the brands in your store with their linked products. For example:

```json title="Example Response"
{
  "brands": [
    {
      "id": "123",
      // ...
      "products": [
        {
          "id": "prod_123",
          // ...
        }
      ]
    }
  ]
}
```

### Limitations: Filtering by Brand in Query

While you can use Query to retrieve linked records, you can't filter by linked records.

For an alternative approach, refer to the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md).

***

## Summary

By following the examples of the previous chapters, you:

- Defined a link between the Brand and Product modules's data models, allowing you to associate a product with a brand.
- Extended the create-product workflow and route to allow setting the product's brand while creating the product.
- Queried a product's brand, and vice versa.

***

## Next Steps: Customize Medusa Admin

Client applications, such as the Medusa Admin dashboard, can now use brand-related features, such as creating a brand or setting the brand of a product.

In the next chapters, you'll learn how to customize the Medusa Admin to show a product's brand on its details page, and to show a new page with the list of brands in your store.


# Guide: Sync Brands from Medusa to Third-Party

In the [previous chapter](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md), you created a CMS Module that integrates a dummy third-party system. You can now perform actions using that module within your custom flows.

In another previous chapter, you [added a workflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) that creates a brand. After integrating the CMS, you want to sync that brand to the third-party system as well.

Medusa has an event system that emits events when an operation is performed. It allows you to listen to those events and perform an asynchronous action in a function called a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). This is useful to perform actions that aren't integral to the original flow, such as syncing data to a third-party system.

Learn more about Medusa's event system and subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md).

In this chapter, you'll modify the `createBrandWorkflow` you created before to emit a custom event that indicates a brand was created. Then, you'll listen to that event in a subscriber to sync the brand to the third-party CMS. You'll implement the sync logic within a workflow that you execute in the subscriber.

### Prerequisites

- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md)
- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md)

## 1. Emit Event in createBrandWorkflow

Since syncing the brand to the third-party system isn't integral to creating a brand, you'll emit a custom event indicating that a brand was created.

Medusa provides an `emitEventStep` that allows you to emit an event in your workflows. So, in the `createBrandWorkflow` defined in `src/workflows/create-brand.ts`, use the `emitEventStep` helper step after the `createBrandStep`:

```ts title="src/workflows/create-brand.ts" highlights={eventHighlights}
// other imports...
import { 
  emitEventStep,
} from "@medusajs/medusa/core-flows"

// ...

export const createBrandWorkflow = createWorkflow(
  "create-brand",
  (input: CreateBrandInput) => {
    // ...

    emitEventStep({
      eventName: "brand.created",
      data: {
        id: brand.id,
      },
    })

    return new WorkflowResponse(brand)
  }
)
```

The `emitEventStep` accepts an object parameter having two properties:

- `eventName`: The name of the event to emit. You'll use this name later to listen to the event in a subscriber.
- `data`: The data payload to emit with the event. This data is passed to subscribers that listen to the event. You add the brand's ID to the data payload, informing the subscribers which brand was created.

You'll learn how to handle this event in a later step.

***

## 2. Create Sync to Third-Party System Workflow

The subscriber that will listen to the `brand.created` event will sync the created brand to the third-party CMS. So, you'll implement the syncing logic in a workflow, then execute the workflow in the subscriber.

Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution.

Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md).

You'll create a `syncBrandToSystemWorkflow` that has two steps:

- `useQueryGraphStep`: a step that Medusa provides to retrieve data using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). You'll use this to retrieve the brand's details using its ID.
- `syncBrandToCmsStep`: a step that you'll create to sync the brand to the CMS.

### syncBrandToCmsStep

To implement the step that syncs the brand to the CMS, create the file `src/workflows/sync-brands-to-cms.ts` with the following content:

![Directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493547/Medusa%20Book/cms-dir-overview-4_u5t0ug.jpg)

```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncStepHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { InferTypeOf } from "@medusajs/framework/types"
import { Brand } from "../modules/brand/models/brand"
import { CMS_MODULE } from "../modules/cms"
import CmsModuleService from "../modules/cms/service"

type SyncBrandToCmsStepInput = {
  brand: InferTypeOf<typeof Brand>
}

const syncBrandToCmsStep = createStep(
  "sync-brand-to-cms",
  async ({ brand }: SyncBrandToCmsStepInput, { container }) => {
    const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE)

    await cmsModuleService.createBrand(brand)

    return new StepResponse(null, brand.id)
  },
  async (id, { container }) => {
    if (!id) {
      return
    }

    const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE)

    await cmsModuleService.deleteBrand(id)
  }
)
```

You create the `syncBrandToCmsStep` that accepts a brand as an input. In the step, you resolve the CMS Module's service from the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) and use its `createBrand` method. This method will create the brand in the third-party CMS.

You also pass the brand's ID to the step's compensation function. In this function, you delete the brand in the third-party CMS if an error occurs during the workflow's execution.

Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md).

### Create Workflow

You can now create the workflow that uses the above step. Add the workflow to the same `src/workflows/sync-brands-to-cms.ts` file:

```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncWorkflowHighlights}
// other imports...
import { 
  // ...
  createWorkflow, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

type SyncBrandToCmsWorkflowInput = {
  id: string
}

export const syncBrandToCmsWorkflow = createWorkflow(
  "sync-brand-to-cms",
  (input: SyncBrandToCmsWorkflowInput) => {
    const { data: brands } = useQueryGraphStep({
      entity: "brand",
      fields: ["*"],
      filters: {
        id: input.id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    syncBrandToCmsStep({
      brand: brands[0],
    } as SyncBrandToCmsStepInput)

    return new WorkflowResponse({})
  }
)
```

You create a `syncBrandToCmsWorkflow` that accepts the brand's ID as input. The workflow has the following steps:

- `useQueryGraphStep`: Retrieve the brand's details using Query. You pass the brand's ID as a filter, and set the `throwIfKeyNotFound` option to true so that the step throws an error if a brand with the specified ID doesn't exist.
- `syncBrandToCmsStep`: Create the brand in the third-party CMS.

You'll execute this workflow in the subscriber next.

Learn more about `useQueryGraphStep` in [this reference](https://docs.medusajs.com/resources/references/helper-steps/useQueryGraphStep/index.html.md).

***

## 3. Handle brand.created Event

You now have a workflow with the logic to sync a brand to the CMS. You need to execute this workflow whenever the `brand.created` event is emitted. So, you'll create a subscriber that listens to and handle the event.

Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, create the file `src/subscribers/brand-created.ts` with the following content:

![Directory structure of the Medusa application after adding the subscriber](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493774/Medusa%20Book/cms-dir-overview-5_iqqwvg.jpg)

```ts title="src/subscribers/brand-created.ts" highlights={subscriberHighlights}
import type {
  SubscriberConfig,
  SubscriberArgs,
} from "@medusajs/framework"
import { syncBrandToCmsWorkflow } from "../workflows/sync-brands-to-cms"

export default async function brandCreatedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await syncBrandToCmsWorkflow(container).run({
    input: data,
  })
}

export const config: SubscriberConfig = {
  event: "brand.created",
}
```

A subscriber file must export:

- The asynchronous function that's executed when the event is emitted. This must be the file's default export.
- An object that holds the subscriber's configurations. It has an `event` property that indicates the name of the event that the subscriber is listening to.

The subscriber function accepts an object parameter that has two properties:

- `event`: An object of event details. Its `data` property holds the event's data payload, which is the brand's ID.
- `container`: The Medusa container used to resolve Framework and commerce tools.

In the function, you execute the `syncBrandToCmsWorkflow`, passing it the data payload as an input. So, everytime a brand is created, Medusa will execute this function, which in turn executes the workflow to sync the brand to the CMS.

Learn more about subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md).

***

## Test it Out

To test the subscriber and workflow out, you'll use the [Create Brand API route](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md) you created in a previous chapter.

First, start the Medusa application:

```bash npm2yarn
npm run dev
```

Since the `/admin/brands` API route has a `/admin` prefix, it's only accessible by authenticated admin users. So, to retrieve an authenticated token of your admin user, send a `POST` request to the `/auth/user/emailpass` API Route:

```bash
curl -X POST 'http://localhost:9000/auth/user/emailpass' \
-H 'Content-Type: application/json' \
--data-raw '{
    "email": "admin@medusa-test.com",
    "password": "supersecret"
}'
```

Make sure to replace the email and password with your admin user's credentials.

Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md).

Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header:

```bash
curl -X POST 'http://localhost:9000/admin/brands' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data '{
    "name": "Acme"
}'
```

This request returns the created brand. If you check the logs, you'll find the `brand.created` event was emitted, and that the request to the third-party system was simulated:

```plain
info:    Processing brand.created which has 1 subscribers
http:    POST /admin/brands ← - (200) - 16.418 ms
info:    Sending a POST request to /brands.
info:    Request Data: {
  "id": "01JEDWENYD361P664WRQPMC3J8",
  "name": "Acme",
  "created_at": "2024-12-06T11:42:32.909Z",
  "updated_at": "2024-12-06T11:42:32.909Z",
  "deleted_at": null
}
info:    API Key: "123"
```

***

## Next Chapter: Sync Brand from Third-Party CMS to Medusa

You can also automate syncing data from a third-party system to Medusa at a regular interval. In the next chapter, you'll learn how to sync brands from the third-party CMS to Medusa once a day.


# Integrate Third-Party Systems

Commerce applications often connect to third-party systems that provide additional or specialized features. For example, you may integrate a Content-Management System (CMS) for rich content features, a payment provider to process credit-card payments, and a notification service to send emails.

The Medusa Framework facilitates integrating these systems and orchestrating operations across them, saving you the effort of managing them yourself. You won't find those capabilities in other commerce platforms that in these scenarios become a bottleneck to building customizations and iterating quickly.

In Medusa, you integrate a third-party system by:

1. Creating a module whose service provides the methods to connect to and perform operations in the third-party system.
2. Building workflows that complete tasks spanning across systems. You use the module that integrates a third-party system in the workflow's steps.
3. Executing the workflows you built in an [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), at a scheduled time, or when an event is emitted.

***

## Next Chapters: Sync Brands Example

In the previous chapters, you've [added brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) to your Medusa application. In the next chapters, you will:

1. Integrate a dummy third-party CMS in the Brand Module.
2. Sync brands to the CMS when a brand is created.
3. Sync brands from the CMS at a daily schedule.


# Guide: Schedule Syncing Brands from Third-Party

In the previous chapters, you've [integrated a third-party CMS](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) and implemented the logic to [sync created brands](https://docs.medusajs.com/learn/customization/integrate-systems/handle-event/index.html.md) from Medusa to the CMS.

However, when you integrate a third-party system, you want the data to be in sync between the Medusa application and the system. One way to do so is by automatically syncing the data once a day.

You can create an action to be automatically executed at a specified interval using scheduled jobs. A scheduled job is an asynchronous function with a specified schedule of when the Medusa application should run it. Scheduled jobs are useful to automate repeated tasks.

Learn more about scheduled jobs in [this chapter](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md).

In this chapter, you'll create a scheduled job that triggers syncing the brands from the third-party CMS to Medusa once a day. You'll implement the syncing logic in a workflow, and execute that workflow in the scheduled job.

### Prerequisites

- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md)

***

## 1. Implement Syncing Workflow

You'll start by implementing the syncing logic in a workflow, then execute the workflow later in the scheduled job.

Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution.

Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md).

This workflow will have three steps:

1. `retrieveBrandsFromCmsStep` to retrieve the brands from the CMS.
2. `createBrandsStep` to create the brands retrieved in the first step that don't exist in Medusa.
3. `updateBrandsStep` to update the brands retrieved in the first step that exist in Medusa.

### retrieveBrandsFromCmsStep

To create the step that retrieves the brands from the third-party CMS, create the file `src/workflows/sync-brands-from-cms.ts` with the following content:

![Directory structure of the Medusa application after creating the file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494196/Medusa%20Book/cms-dir-overview-6_z1omsi.jpg)

```ts title="src/workflows/sync-brands-from-cms.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports"
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import CmsModuleService from "../modules/cms/service"
import { CMS_MODULE } from "../modules/cms"

const retrieveBrandsFromCmsStep = createStep(
  "retrieve-brands-from-cms",
  async (_, { container }) => {
    const cmsModuleService: CmsModuleService = container.resolve(
      CMS_MODULE
    )

    const brands = await cmsModuleService.retrieveBrands()

    return new StepResponse(brands)
  }
)
```

You create a `retrieveBrandsFromCmsStep` that resolves the CMS Module's service and uses its `retrieveBrands` method to retrieve the brands in the CMS. You return those brands in the step's response.

### createBrandsStep

The brands retrieved in the first step may have brands that don't exist in Medusa. So, you'll create a step that creates those brands. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file:

```ts title="src/workflows/sync-brands-from-cms.ts" highlights={createBrandsHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports"
// other imports...
import BrandModuleService from "../modules/brand/service"
import { BRAND_MODULE } from "../modules/brand"

// ...

type CreateBrand = {
  name: string
}

type CreateBrandsInput = {
  brands: CreateBrand[]
}

export const createBrandsStep = createStep(
  "create-brands-step",
  async (input: CreateBrandsInput, { container }) => {
    const brandModuleService: BrandModuleService = container.resolve(
      BRAND_MODULE
    )

    const brands = await brandModuleService.createBrands(input.brands)

    return new StepResponse(brands, brands)
  },
  async (brands, { container }) => {
    if (!brands) {
      return
    }

    const brandModuleService: BrandModuleService = container.resolve(
      BRAND_MODULE
    )

    await brandModuleService.deleteBrands(brands.map((brand) => brand.id))
  }
)
```

The `createBrandsStep` accepts the brands to create as an input. It resolves the [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)'s service and uses the generated `createBrands` method to create the brands.

The step passes the created brands to the compensation function, which deletes those brands if an error occurs during the workflow's execution.

Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md).

### Update Brands Step

The brands retrieved in the first step may also have brands that exist in Medusa. So, you'll create a step that updates their details to match that of the CMS. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file:

```ts title="src/workflows/sync-brands-from-cms.ts" highlights={updateBrandsHighlights}
// ...

type UpdateBrand = {
  id: string
  name: string
}

type UpdateBrandsInput = {
  brands: UpdateBrand[]
}

export const updateBrandsStep = createStep(
  "update-brands-step",
  async ({ brands }: UpdateBrandsInput, { container }) => {
    const brandModuleService: BrandModuleService = container.resolve(
      BRAND_MODULE
    )

    const prevUpdatedBrands = await brandModuleService.listBrands({
      id: brands.map((brand) => brand.id),
    })

    const updatedBrands = await brandModuleService.updateBrands(brands)

    return new StepResponse(updatedBrands, prevUpdatedBrands)
  },
  async (prevUpdatedBrands, { container }) => {
    if (!prevUpdatedBrands) {
      return
    }

    const brandModuleService: BrandModuleService = container.resolve(
      BRAND_MODULE
    )

    await brandModuleService.updateBrands(prevUpdatedBrands)
  }
)
```

The `updateBrandsStep` receives the brands to update in Medusa. In the step, you retrieve the brand's details in Medusa before the update to pass them to the compensation function. You then update the brands using the Brand Module's `updateBrands` generated method.

In the compensation function, which receives the brand's old data, you revert the update using the same `updateBrands` method.

### Create Workflow

Finally, you'll create the workflow that uses the above steps to sync the brands from the CMS to Medusa. Add to the same `src/workflows/sync-brands-from-cms.ts` file the following:

```ts title="src/workflows/sync-brands-from-cms.ts"
// other imports...
import {
  // ...
  createWorkflow,
  transform,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

// ...

export const syncBrandsFromCmsWorkflow = createWorkflow(
  "sync-brands-from-system",
  () => {
    const brands = retrieveBrandsFromCmsStep()

    // TODO create and update brands
  }
)
```

In the workflow, you only use the `retrieveBrandsFromCmsStep` for now, which retrieves the brands from the third-party CMS.

Next, you need to identify which brands must be created or updated. Since workflows are constructed internally and are only evaluated during execution, you can't access values to perform data manipulation directly. Instead, use [transform](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK that gives you access to the real-time values of the data, allowing you to create new variables using those values.

Learn more about data manipulation using `transform` in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md).

So, replace the `TODO` with the following:

```ts title="src/workflows/sync-brands-from-cms.ts"
const { toCreate, toUpdate } = transform(
  {
    brands,
  },
  (data) => {
    const toCreate: CreateBrand[] = []
    const toUpdate: UpdateBrand[] = []

    data.brands.forEach((brand) => {
      if (brand.external_id) {
        toUpdate.push({
          id: brand.external_id as string,
          name: brand.name as string,
        })
      } else {
        toCreate.push({
          name: brand.name as string,
        })
      }
    })

    return { toCreate, toUpdate }
  }
)

// TODO create and update the brands
```

`transform` accepts two parameters:

1. The data to be passed to the function in the second parameter.
2. A function to execute only when the workflow is executed. Its return value can be consumed by the rest of the workflow.

In `transform`'s function, you loop over the brands array to check which should be created or updated. This logic assumes that a brand in the CMS has an `external_id` property whose value is the brand's ID in Medusa.

You now have the list of brands to create and update. So, replace the new `TODO` with the following:

```ts title="src/workflows/sync-brands-from-cms.ts"
const created = createBrandsStep({ brands: toCreate })
const updated = updateBrandsStep({ brands: toUpdate })

return new WorkflowResponse({
  created,
  updated,
})
```

You first run the `createBrandsStep` to create the brands that don't exist in Medusa, then the `updateBrandsStep` to update the brands that exist in Medusa. You pass the arrays returned by `transform` as the inputs for the steps.

Finally, you return an object of the created and updated brands. You'll execute this workflow in the scheduled job next.

***

## 2. Schedule Syncing Task

You now have the workflow to sync the brands from the CMS to Medusa. Next, you'll create a scheduled job that runs this workflow once a day to ensure the data between Medusa and the CMS are always in sync.

A scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory. So, create the file `src/jobs/sync-brands-from-cms.ts` with the following content:

![Directory structure of the Medusa application after adding the scheduled job](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494592/Medusa%20Book/cms-dir-overview-7_dkjb9s.jpg)

```ts title="src/jobs/sync-brands-from-cms.ts"
import { MedusaContainer } from "@medusajs/framework/types"
import { syncBrandsFromCmsWorkflow } from "../workflows/sync-brands-from-cms"

export default async function (container: MedusaContainer) {
  const logger = container.resolve("logger")

  const { result } = await syncBrandsFromCmsWorkflow(container).run()

  logger.info(
    `Synced brands from third-party system: ${
      result.created.length
    } brands created and ${result.updated.length} brands updated.`)
}

export const config = {
  name: "sync-brands-from-system",
  schedule: "0 0 * * *", // change to * * * * * for debugging
}
```

A scheduled job file must export:

- An asynchronous function that will be executed at the specified schedule. This function must be the file's default export.
- An object of scheduled jobs configuration. It has two properties:
  - `name`: A unique name for the scheduled job.
  - `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job.

The scheduled job function accepts as a parameter the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) used to resolve Framework and commerce tools. You then execute the `syncBrandsFromCmsWorkflow` and use its result to log how many brands were created or updated.

Based on the cron expression specified in `config.schedule`, Medusa will run the scheduled job every day at midnight. You can also change it to `* * * * *` to run it every minute for easier debugging.

***

## Test it Out

To test out the scheduled job, start the Medusa application:

```bash npm2yarn
npm run dev
```

If you set the schedule to `* * * * *` for debugging, the scheduled job will run in a minute. You'll see in the logs how many brands were created or updated.

***

## Summary

By following the previous chapters, you utilized the Medusa Framework and orchestration tools to perform and automate tasks that span across systems.

With Medusa, you can integrate any service from your commerce ecosystem with ease. You don't have to set up separate applications to manage your different customizations, or worry about data inconsistency across systems. Your efforts only go into implementing the business logic that ties your systems together.


# Guide: Integrate Third-Party Brand System

In the previous chapters, you've created a [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) that adds brands to your application. In this chapter, you'll integrate a dummy Content-Management System (CMS) in a new module. The module's service will provide methods to retrieve and manage brands in the CMS. You'll later use this service to sync data from and to the CMS.

Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md).

## 1. Create Module Directory

You'll integrate the third-party system in a new CMS Module. So, create the directory `src/modules/cms` that will hold the module's resources.

![Directory structure after adding the directory for the CMS Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492447/Medusa%20Book/cms-dir-overview-1_gasguk.jpg)

***

## 2. Create Module Service

Next, you'll create the module's service. It will provide methods to connect and perform actions with the third-party system.

Create the CMS Module's service at `src/modules/cms/service.ts` with the following content:

![Directory structure after adding the CMS Module's service](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492583/Medusa%20Book/cms-dir-overview-2_zwcwh3.jpg)

```ts title="src/modules/cms/service.ts" highlights={serviceHighlights}
import { Logger, ConfigModule } from "@medusajs/framework/types"

export type ModuleOptions = {
  apiKey: string
}

type InjectedDependencies = {
  logger: Logger
  configModule: ConfigModule
}

class CmsModuleService {
  private options_: ModuleOptions
  private logger_: Logger

  constructor({ logger }: InjectedDependencies, options: ModuleOptions) {
    this.logger_ = logger
    this.options_ = options

    // TODO initialize SDK
  }
}

export default CmsModuleService
```

You create a `CmsModuleService` that will hold the methods to connect to the third-party CMS. A service's constructor accepts two parameters:

1. The module's container. Since a module is [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), it has a [local container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md) different than the Medusa container you use in other customizations. This container holds Framework tools like the [Logger utility](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) and resources within the module.
2. Options passed to the module when it's later added in Medusa's configurations. These options are useful to pass secret keys or configurations that ensure your module is re-usable across applications. For the CMS Module, you accept the API key to connect to the dummy CMS as an option.

When integrating a third-party system that has a Node.js SDK or client, you can initialize that client in the constructor to be used in the service's methods.

### Integration Methods

Next, you'll add methods that simulate sending requests to a third-party CMS. You'll use these methods later to sync brands from and to the CMS.

Add the following methods in the `CmsModuleService`:

```ts title="src/modules/cms/service.ts" highlights={methodsHighlights}
export class CmsModuleService {
  // ...

  // a dummy method to simulate sending a request,
  // in a realistic scenario, you'd use an SDK, fetch, or axios clients
  private async sendRequest(url: string, method: string, data?: any) {
    this.logger_.info(`Sending a ${method} request to ${url}.`)
    this.logger_.info(`Request Data: ${JSON.stringify(data, null, 2)}`)
    this.logger_.info(`API Key: ${JSON.stringify(this.options_.apiKey, null, 2)}`)
  }

  async createBrand(brand: Record<string, unknown>) {
    await this.sendRequest("/brands", "POST", brand)
  }

  async deleteBrand(id: string) {
    await this.sendRequest(`/brands/${id}`, "DELETE")
  }

  async retrieveBrands(): Promise<Record<string, unknown>[]> {
    await this.sendRequest("/brands", "GET")

    return []
  }
}
```

The `sendRequest` method sends requests to the third-party CMS. Since this guide isn't using a real CMS, it only simulates the sending by logging messages in the terminal.

You also add three methods that use the `sendRequest` method:

- `createBrand` that creates a brand in the third-party system.
- `deleteBrand` that deletes the brand in the third-party system.
- `retrieveBrands` to retrieve a brand from the third-party system.

***

## 3. Export Module Definition

After creating the module's service, you'll export the module definition indicating the module's name and service.

Create the file `src/modules/cms/index.ts` with the following content:

![Directory structure of the Medusa application after adding the module definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492991/Medusa%20Book/cms-dir-overview-3_b0byks.jpg)

```ts title="src/modules/cms/index.ts"
import { Module } from "@medusajs/framework/utils"
import CmsModuleService from "./service"

export const CMS_MODULE = "cms"

export default Module(CMS_MODULE, {
  service: CmsModuleService,
})
```

You use `Module` from the Modules SDK to export the module's defintion, indicating that the module's name is `cms` and its service is `CmsModuleService`.

***

## 4. Add Module to Medusa's Configurations

Finally, add the module to the Medusa configurations at `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    // ...
    {
      resolve: "./src/modules/cms",
      options: {
        apiKey: process.env.CMS_API_KEY,
      },
    },
  ], 
})
```

The object passed in `modules` accept an `options` property, whose value is an object of options to pass to the module. These are the options you receive in the `CmsModuleService`'s constructor.

You can add the `CMS_API_KEY` environment variable to `.env`:

```bash
CMS_API_KEY=123
```

***

## Next Steps: Sync Brand From Medusa to CMS

You can now use the CMS Module's service to perform actions on the third-party CMS.

In the next chapter, you'll learn how to emit an event when a brand is created, then handle that event to sync the brand from Medusa to the third-party service.


# Customizations Next Steps: Learn the Fundamentals

The previous guides introduced Medusa's different concepts and how you can use them to customize Medusa for a realistic use case, You added brands to your application, linked them to products, customized the admin dashboard, and integrated a third-party CMS.

The next chapters will cover each of these concepts in depth, with the different ways you can use them, their options or configurations, and more advanced features that weren't covered in the previous guides. While you can start building with Medusa, it's highly recommended to follow the next chapters for a better understanding of Medusa's fundamentals.

## Useful Guides

The following guides and references are useful for your development journey:

3. [Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md): Browse the list of Commerce Modules in Medusa and their references to learn how to use them.
4. [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md): Learn about the methods generated by `MedusaService` with examples.
5. [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md): Browse the list of core workflows and their hooks that are useful for your customizations.
6. [Admin Injection Zones](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md): Browse the injection zones in the Medusa Admin to learn where you can inject widgets.

***

## More Examples in Recipes

In the [Recipes](https://docs.medusajs.com/resources/recipes/index.html.md) documentation, you'll also find step-by-step guides for different use cases, such as building a marketplace, digital products, and more.


# Re-Use Customizations with Plugins

In the previous chapters, you've followed the [brand example](https://docs.medusajs.com/learn/customization/custom-features/index.html.md) to learn important concepts related to:

- [Creating modules](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md).
- [Implementing commerce features in workflows](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md).
- [Exposing those features in API routes](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md).
- [Customizing the Medusa Admin dashboard with Admin Extensions](https://docs.medusajs.com/learn/customization/customize-admin/index.html.md).
- [Integrating third-party systems](https://docs.medusajs.com/learn/customization/integrate-systems/index.html.md).

You've implemented the brands example within a single Medusa application. However, this approach is not scalable. If you want to use the same brands feature in different Medusa applications, you would have to manually copy the code and maintain it across all projects.

Instead, to reuse your customizations across multiple Medusa applications, you can create a plugin. A plugin is an NPM package that encapsulates your customizations and can be installed in any Medusa application. Plugins can include modules, workflows, API routes, Admin Extensions, and more.

![Diagram showcasing how the Brand Plugin would add its resources to any application it's installed in](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540091/Medusa%20Book/brand-plugin_bk9zi9.jpg)

Medusa provides the tooling to create a plugin package, test it in a local Medusa application, and publish it to NPM.

Refer to the [Plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) chapter to learn more about plugins and how to create them.


# Debug Workflows

In this chapter, you'll learn about the different ways you can debug workflows in Medusa.

Debugging workflows is essential to ensure your custom features work as expected. It helps you identify unexpected issues and bugs in your workflow logic.

## Approaches to Debug Workflows

There are several ways to debug workflows in Medusa:

|Approach|When to Use|
|---|---|
|Write integration tests|To ensure your workflow produces the expected results and handles edge cases.|
|Add breakpoints|To inspect specific steps in your workflow and understand the data flow.|
|Log messages|To check values during execution with minimal overhead.|
|View Workflow Executions in Medusa Admin|To monitor stored workflow executions and long-running workflows, especially in production environments.|

***

## Approach 1: Write Integration Tests

Integration tests run your workflow in a controlled environment to verify its behavior and outcome. By writing integration tests, you ensure your workflow produces the expected results and handles edge cases.

### When to Use Integration Tests

It's recommended to always write integration tests for your workflows. This helps you catch issues early and ensures your custom logic works as intended.

### How to Write Integration Tests for Workflows

Refer to the [Integration Tests](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) chapter to learn how to write integration tests for your workflows and find examples of testing workflows.

***

## Approach 2: Add Breakpoints

Breakpoints allow you to pause workflow execution at specific steps and inspect the data. They're useful for understanding the data flow in your steps and identifying issues.

### When to Use Breakpoints

Use breakpoints when you need to debug specific steps in your workflow, rather than the entire workflow. You can verify that the step is behaving as expected and is producing the correct output.

### Where Can You Add Breakpoints

Since Medusa stores an internal representation of the workflow constructor on application startup, breakpoints within the workflow's constructor won't work during execution. Learn more in the [Data Manipulation](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) chapter.

Instead, you can add breakpoints in:

- A step function.
- A step's compensation function.
- The `transform` callback function of a step.

For example:

### Step Function

```ts highlights={[["11"], ["12"]]} collapsibleLines="1-7" expandButtonLabel="Show Imports"
import { 
  createStep, 
  createWorkflow, 
  StepResponse, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async () => {
    // Add a breakpoint here to inspect the message
    const message = "Hello from step 1!"

    return new StepResponse(
      message
    )
  }
)

export const myWorkflow = createWorkflow(
  "my-workflow",
  () => {
    const response = step1()
    
    return new WorkflowResponse({
      response,
    })
  }
)
```

### Compensation Function

```ts highlights={[["18"], ["19"]]} collapsibleLines="1-7" expandButtonLabel="Show Imports"
import { 
  createStep, 
  createWorkflow, 
  StepResponse, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async () => {
    const message = "Hello from step 1!"

    return new StepResponse(
      message
    )
  },
  async () => {
    // Add a breakpoint here to inspect the compensation logic
    console.log("Compensating step 1")
  }
)

const step2 = createStep(
  "step-2",
  async () => {
    throw new Error("This is an error in step 2")
  }
)

export const myWorkflow = createWorkflow(
  "my-workflow",
  () => {
    const response = step1()
    step2()

    return new WorkflowResponse({
      response,
    })
  }
)
```

### Transform Callback

```ts highlights={[["28"], ["29"]]} collapsibleLines="1-8" expandButtonLabel="Show Imports"
import { 
  createStep, 
  createWorkflow, 
  StepResponse, 
  WorkflowResponse,
  transform,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async () => {
    const message = "Hello from step 1!"

    return new StepResponse(
      message
    )
  }
)

export const myWorkflow = createWorkflow(
  "my-workflow",
  () => {
    const response = step1()

    const transformedMessage = transform(
      { response },
      (data) => {
        // Add a breakpoint here to inspect the transformed data
        const upperCase = data.response.toUpperCase()
        return upperCase
      }
    )

    return new WorkflowResponse({
      response: transformedMessage,
    })
  }
)
```

### How to Add Breakpoints

If your code editor supports adding breakpoints, you can add them in your step and compensation functions, or the `transform` callback function. When the workflow execution reaches the breakpoint, your code editor will pause execution, allowing you to inspect the data and walk through the code.

If you're using VS Code or Cursor, learn how to add breakpoints in the [VS Code documentation](https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_breakpoints). For other code editors, refer to their respective documentation.

***

## Approach 3: Log Messages

Logging messages is a simple yet effective way to debug code. By logging messages, you can check values during execution with minimal overhead.

### When to Use Logging

Use logging when debugging workflows and you want to check values during execution without the overhead of setting up breakpoints.

Logging is also useful when you want to verify variable values between steps or in a `transform` callback function.

### How to Log Messages

Since Medusa stores an internal representation of the workflow constructor on application startup, you can't directly log messages in the workflow's constructor.

Instead, you can log messages in:

- A step function.
- A step's compensation function.
- The `transform` callback function of a step.

You can log messages with `console.log`. In step and compensation functions, you can also use the [Logger](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) to log messages with different log levels (info, warn, error).

For example:

### Step Function

```ts highlights={[["14"]]} collapsibleLines="1-7" expandButtonLabel="Show Imports"
import { 
  createStep, 
  createWorkflow, 
  StepResponse, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async ({}, { container }) => {
    const logger = container.resolve("logger")
    const message = "Hello from step 1!"

    logger.info(`Step 1 output: ${message}`)

    return new StepResponse(
      message
    )
  }
)

export const myWorkflow = createWorkflow(
  "my-workflow",
  () => {
    const response = step1()

    return new WorkflowResponse({
      response,
    })
  }
)
```

### Compensation Function

```ts highlights={[["22"]]} collapsibleLines="1-7" expandButtonLabel="Show Imports"
import { 
  createStep, 
  createWorkflow, 
  StepResponse, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async ({}, { container }) => {
    const logger = container.resolve("logger")
    const message = "Hello from step 1!"

    logger.info(`Step 1 output: ${message}`)

    return new StepResponse(
      message
    )
  },
  async (_, { container }) => {
    const logger = container.resolve("logger")
    logger.warn("Compensating step 1")
  }
)

export const myWorkflow = createWorkflow(
  "my-workflow",
  () => {
    const response = step1()

    return new WorkflowResponse({
      response,
    })
  }
)
```

### Transform Callback

```ts highlights={[["29"]]} collapsibleLines="1-8" expandButtonLabel="Show Imports"
import { 
  createStep, 
  createWorkflow, 
  StepResponse, 
  WorkflowResponse,
  transform,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async () => {
    const message = "Hello from step 1!"

    return new StepResponse(
      message
    )
  }
)

export const myWorkflow = createWorkflow(
  "my-workflow",
  () => {
    const response = step1()

    const transformedMessage = transform(
      { response },
      (data) => {
        const upperCase = data.response.toUpperCase()
        console.log("Transformed Data:", upperCase)
        return upperCase
      }
    )

    return new WorkflowResponse({
      response: transformedMessage,
    })
  }
)
```

If you execute the workflow, you'll see the logged message in your console.

Learn more about logging in the [Logger](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) chapter.

***

## Approach 4: Monitor Workflow Executions in Medusa Admin

The Medusa Admin has a [Workflows](https://docs.medusajs.com/user-guide/settings/developer/workflows/index.html.md) settings page that provides a user-friendly interface to view stored workflow executions.

### When to Use Admin Monitoring

Use the Medusa Admin to monitor [stored workflow executions](https://docs.medusajs.com/learn/fundamentals/workflows/store-executions/index.html.md) when debugging unexpected issues and edge cases, especially in production environments and long-running workflows that run in the background.

By viewing the workflow executions through the Medusa Admin, you can:

- View the status of stored workflow executions.
- Inspect input and output data for each execution and its steps.
- Identify any issues or errors in the workflow execution.

### How to Monitor Workflow Executions in the Admin

The Workflows settings page in the Medusa Admin shows you the history of stored workflow executions only. Workflow executions are stored if a workflow is [long-running](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md), or if the `store` and `retentionTime` options are set on the workflow.

For example, to store workflow executions:

### Prerequisites

- [Redis Workflow Engine must be installed and configured.](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md)

```ts highlights={[["22"], ["23"]]} collapsibleLines="1-7" expandButtonLabel="Show Imports"
import { 
  createStep, 
  createWorkflow, 
  StepResponse, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async () => {
    const message = "Hello from step 1!"

    return new StepResponse(
      message
    )
  }
)

export const myWorkflow = createWorkflow(
  {
    name: "my-workflow",
    retentionTime: 99999,
    store: true,
  },
  () => {
    const response = step1()

    return new WorkflowResponse({
      response,
    })
  }
)
```

Refer to the [Store Workflow Executions](https://docs.medusajs.com/learn/fundamentals/workflows/store-executions/index.html.md) chapter to learn more.

You can view all executions of this workflow in the Medusa Admin under the [Workflows settings page](https://docs.medusajs.com/user-guide/settings/developer/workflows/index.html.md). Each execution will show you the status, input, and output data.


# Create Custom Feature Flag

In this chapter, you'll learn how to create a custom feature flag in Medusa.

## Why Create a Custom Feature Flag?

As explained in the [Feature Flags](https://docs.medusajs.com/learn/debugging-and-testing/feature-flags/index.html.md) chapter, feature flags allow the Medusa team to ship new features that are still under development and testing, but not ready for production or wide use yet.

You can also create custom feature flags for your Medusa project or plugin to control the availability of experimental or in-development features. Feature flags allow you to test features in staging, and only enable them in production when they are ready.

***

## How to Create a Custom Feature Flag

### 1. Define the Feature Flag

To create a custom feature flag, you need to define it in a new file under the `src/feature-flags` directory of your Medusa project or plugin. The file must export a configuration object.

For example, to define a feature flag that enables a blog feature, create the file `src/feature-flags/blog.ts` with the following content:

```ts title="src/feature-flags/blog.ts" highlights={featureFlagHighlights}
import { FlagSettings } from "@medusajs/framework/feature-flags"

const BlogFeatureFlag: FlagSettings = {
  key: "blog_feature",
  default_val: false,
  env_key: "FF_BLOG_FEATURE",
  description: "Enable blog features",
}

export default BlogFeatureFlag
```

The feature flag configuration is an object having the following properties:

- key: (\`string\`) The unique identifier for the feature flag. This key is used to enable or disable the feature flag and check its status.
- default\_val: (\`boolean\`) Whether the feature flag is enabled by default.
- env\_key: (\`string\`) The environment variable key for the feature flag. This key is used to enable or disable the feature flag using environment variables.
- description: (\`string\`) A brief description of what the feature flag does.

### 2. Hide Features Behind the Flag

Next, you can build the features you want to hide behind the feature flag.

To build backend customizations around feature flags, you can either:

- [Conditionally run code blocks](https://docs.medusajs.com/learn/debugging-and-testing/feature-flags#conditionally-run-code-blocks/index.html.md) based on the feature flag status;
- [Conditionally load files](https://docs.medusajs.com/learn/debugging-and-testing/feature-flags#conditionally-load-files/index.html.md) if the feature flag is not enabled.

For client customizations, such as admin widgets, you can use the [Feature Flags API route](https://docs.medusajs.com/learn/debugging-and-testing/feature-flags#feature-flags-api-route/index.html.md).

### 3. Toggle Feature Flag

To enable or disable your custom feature flag, you can either add it to your `medusa-config.ts` file:

```ts title="medusa-config.ts"
import BlogFeatureFlag from "./src/feature-flags/blog"

module.exports = defineConfig({
  // ...
  featureFlags: {
    [BlogFeatureFlag.key]: true,
  },
})
```

Or set the environment variable for the feature flag:

```shell
FF_BLOG_FEATURE=true
```

Afterwards, make sure to [run migrations](https://docs.medusajs.com/learn/fundamentals/data-models/write-migration#run-the-migration/index.html.md) if your feature flag requires database changes.

If you're disabling a feature flag, make sure to [roll back any migrations](https://docs.medusajs.com/learn/fundamentals/data-models/write-migration#rollback-the-migration/index.html.md) that depend on it first.

***

## Write Tests for Features Behind Flags

If you're writing integration tests for features hidden behind feature flags, you can enable the feature flag's environment variable in your integration test using the `env` option.

For example:

```ts title="integration-tests/http/test.spec.ts"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"

medusaIntegrationTestRunner({
  env: {
    FF_BLOG_FEATURE: true,
  },
  testSuite: ({ api, getContainer }) => {
    // TODO write tests...
  },
})
```

Then, the Medusa application will load your customizations hidden behind the feature flag as part of the integration tests.


# Feature Flags

In this chapter, you'll learn what feature flags are, what the available feature flags in Medusa are, and how to toggle them.

## What are Feature Flags?

Feature flags allow you to ship new features that are still under development and testing, but not ready for production or wide use yet.

Medusa uses feature flags to ship new versions even when some features are not fully ready. This approach allows our team to continuously deliver updates and improvements while stabilizing new features.

***

## Available Feature Flags in Medusa

For a list of features hidden behind a feature flag in Medusa, check out the [feature-flags](https://github.com/medusajs/medusa/tree/develop/packages/medusa/src/feature-flags) directory in the `@medusajs/medusa` package.

***

## Toggle Feature Flags

There are multiple ways to enable or disable feature flags in Medusa:

- In the `medusa-config.ts` file;
- Or using environment variables.

### 1. Using the `medusa-config.ts` file

The Medusa configurations in `medusa-config.ts` accept a `featureFlags` configuration to toggle feature flags. Its value is an object whose key is the feature flag key (defined in the feature flag's file), and the value is a boolean indicating whether the feature is enabled.

For example, to enable the [Index Module](https://docs.medusajs.com/learn/fundamentals/module-links/index-module/index.html.md)'s feature flag in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  featureFlags: {
    "index_engine": true,
  },
})
```

Make sure to [run migrations](https://docs.medusajs.com/learn/fundamentals/data-models/write-migration#run-the-migration/index.html.md) after enabling a feature flag, in case it requires database changes.

Before disabling a feature flag, make sure to [roll back the migrations](https://docs.medusajs.com/learn/fundamentals/data-models/write-migration#rollback-the-migration/index.html.md) that depend on it.

### 2. Using Environment Variables

The feature flags can also be enabled or disabled using environment variables. This is useful to control the feature flag based on the environment.

To toggle a feature flag using an environment variable, set an environment variable with its environment variable name (defined in the feature flag's file) and set its value to `true` or `false`.

For example, to enable the [Index Module](https://docs.medusajs.com/learn/fundamentals/module-links/index-module/index.html.md)'s feature flag using an environment variable:

```shell
MEDUSA_FF_INDEX_ENGINE=true
```

Make sure to [run migrations](https://docs.medusajs.com/learn/fundamentals/data-models/write-migration#run-the-migration/index.html.md) after enabling a feature flag, in case it requires database changes.

Before disabling a feature flag, make sure to [roll back the migrations](https://docs.medusajs.com/learn/fundamentals/data-models/write-migration#rollback-the-migration/index.html.md) that depend on it.

***

## Check Feature Flag Status

During development, you can check whether a feature flag is enabled. This is useful to add customizations that only apply when a specific feature is enabled.

To build backend customizations around feature flags, you can either:

- [Conditionally run code blocks](#conditionally-run-code-blocks) with the `FeatureFlag` utility;
- Or [conditionally load files](#conditionally-load-files) with the `defineFileConfig` utility.

For client customizations, you can use the [Feature Flags API Route](#feature-flags-api-route).

### Conditionally Run Code Blocks

The `FeatureFlag` utility allows you to check whether a specific feature flag is enabled in your backend customizations, including scheduled jobs, subscribers, and workflow steps.

For example, to enable access to a route only if a feature flag is enabled, you can use the `FeatureFlag` utility in a [middleware](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md):

```ts title="src/api/middlewares.ts" highlights={middlewareHighlights}
import { defineMiddlewares } from "@medusajs/framework/http"
import { FeatureFlag } from "@medusajs/framework/utils"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom",
      method: ["GET"],
      middlewares: [
        async (req, res, next) => {
          if (!FeatureFlag.isFeatureEnabled("index_engine")) {
            return res.sendStatus(404)
          }
          next()
        },
      ],
    },
  ],
})
```

The `FeatureFlag.isFeatureEnabled` method accepts the feature flag key as a parameter and returns a boolean indicating whether the feature is enabled or not.

In the above example, you return a `404` response if a `GET` request is sent to the `/custom` route and the `index_engine` feature flag is disabled.

### Conditionally Load Files

You can also combine the `FeatureFlag` utility with the `defineFileConfig` utility that allows you to fully enable or disable loading a file based on a condition.

For example, instead of adding a middleware, you can use the `defineFileConfig` utility to conditionally load an API route file only if a feature flag is enabled:

```ts title="src/api/routes/custom.ts" highlights={apiRouteHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
import { defineFileConfig, FeatureFlag } from "@medusajs/framework/utils"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
): Promise<void> {
  res.json({
    message: "Hello World",
  })
}

defineFileConfig({
  isDisabled: () => !FeatureFlag.isFeatureEnabled("index_engine"),
})
```

The `defineFileConfig` function accepts an object with an `isDisabled` property. Its value is a function that returns a boolean indicating whether the file should be disabled.

In the above example, the `GET` API route at `/custom` will only be available if the `index_engine` feature flag is enabled.

While both approaches can be used to disable API routes, a middleware is useful to disable specific HTTP methods at a route. For example, if a route file has `GET` and `POST` route handlers, the `defineFileConfig` approach will disable both `GET` and `POST` methods if the feature flag is disabled. If you need to disable only the `POST` route, you should use a middleware instead.

### Feature Flags API Route

For client customizations, you can use the [List Feature Flags API Route](https://docs.medusajs.com/api/admin#feature-flags_getfeatureflags) to check the status of feature flags.

For example, you can show an [admin widget](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md) only if a feature flag is enabled:

```tsx title="src/admin/widgets/product-details.tsx" highlights={widgetHighlights}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { sdk } from "../lib/sdk"
import { useQuery } from "@tanstack/react-query"

const ProductWidget = () => {
  const { data: featureFlags } = useQuery({
    queryKey: ["featureFlags"],
    queryFn: () => sdk.client.fetch<{ 
      feature_flags: Record<string, boolean>
    }>(`/admin/feature-flags`),
  })

  if (!featureFlags?.feature_flags.index_engine) {
    return null
  }

  return (
    <div>
      <h2>Product Widget</h2>
      <p>Index engine feature is enabled</p>
    </div>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.after",
})

export default ProductWidget
```

In the above example, the Product Widget will only be displayed if the `index_engine` feature flag is enabled. It retrieves the feature flag statuses from the `/admin/feature-flags` API route.


# Configure Instrumentation

In this chapter, you'll learn about observability in Medusa and how to configure instrumentation with OpenTelemetry.

## What is Instrumentation?

Instrumentation is the collection of data about your application's performance and errors. It helps you debug issues, monitor performance, and gain insights into how your application behaves in production.

Instrumentation and observability are crucial as you build customizations in your application. They allow you to optimize performance, identify bottlenecks, and ensure your application runs smoothly.

***

## Instrumentation and Observability with OpenTelemetry

Medusa uses [OpenTelemetry](https://opentelemetry.io/) for instrumentation and reporting. When configured, it reports traces for:

- HTTP requests
- Workflow executions
- Query usages
- Database queries and operations

***

## How to Configure Instrumentation in Medusa?

### Prerequisites

- [An exporter to visualize your application's traces, such as Zipkin.](https://zipkin.io/pages/quickstart.html)

### Install Dependencies

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.

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
```

### Add instrumentation.ts

Next, create the file `instrumentation.ts` with the following content:

```ts title="instrumentation.ts"
import { registerOtel } from "@medusajs/medusa"
import { ZipkinExporter } from "@opentelemetry/exporter-zipkin"

// If using an exporter other than Zipkin, initialize it here.
const exporter = new ZipkinExporter({
  serviceName: "my-medusa-project",
})

export function register() {
  registerOtel({
    serviceName: "medusajs",
    // pass exporter
    exporter,
    instrument: {
      http: true,
      workflows: true,
      query: true,
    },
  })
}
```

In the `instrumentation.ts` file, you export a `register` function that uses Medusa's `registerOtel` utility function. You also initialize an instance of the exporter, such as Zipkin, and pass it to the `registerOtel` function.

`registerOtel` accepts an object where you can pass any [NodeSDKConfiguration](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_sdk-node.NodeSDKConfiguration.html) property along with the following properties:

The `NodeSDKConfiguration` properties are accepted since Medusa v2.5.1.

- serviceName: (\`string\`) The name of the service traced.
- exporter: (\[SpanExporter]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_sdk-node.node.SpanExporter.html)) An instance of an exporter, such as Zipkin.
- instrument: (\`object\`) Options specifying what to trace.

  - http: (\`boolean\`) Whether to trace HTTP requests.

  - query: (\`boolean\`) Whether to trace Query usages.

  - workflows: (\`boolean\`) Whether to trace Workflow executions.

  - db: (\`boolean\`) Whether to trace database queries and operations.
- instrumentations: (\[Instrumentation\[]]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_instrumentation.Instrumentation.html)) Additional instrumentation options that OpenTelemetry accepts.

***

## Test it Out

To test it out, start your exporter, such as Zipkin.

Then, start your Medusa application:

```bash npm2yarn
npm run dev
```

Try to open the Medusa Admin or send a request to an API route.

If you check traces in your exporter, you'll find new traces reported.

### Trace Span Names

Trace span names start with the following keywords based on what it's reporting:

- `{methodName} {URL}` when reporting HTTP requests, where `{methodName}` is the HTTP method, and `{URL}` is the URL the request is sent to.
- `route:` when reporting route handlers running on an HTTP request.
- `middleware:` when reporting a middleware running on an HTTP request.
- `workflow:` when reporting a workflow execution.
- `step:` when reporting a step in a workflow execution.
- `query.graph:` when reporting Query usages.
- `pg.query:` when reporting database queries and operations.

***

## Useful Links

- [Integrate Sentry with Medusa](https://docs.medusajs.com/resources/integrations/guides/sentry/index.html.md)


# Override Logger

In this guide, you'll learn how to override the default [Logger](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) in Medusa with your custom implementation.

## Why Override the Logger?

Medusa's default `Logger` logs your application's events, errors, and general log messages into the console. However, you might want to customize this behavior for your specific use case.

For example, you might want to change the logs format, or add additional metadata to your logs. In those cases, it's useful to override the default `Logger` with your custom implementation.

After overriding the `Logger`, Medusa will use your custom logger whenever it needs to log a message.

***

## How to Override the Logger?

To override the default `Logger`:

1. Create a class that extends the `Logger` interface from `@medusajs/framework/types`.
2. Pass the class in the Medusa configurations.

### Step 1: Create a Custom Logger Class

To create a custom logger class, create a new file at `src/utils/custom-logger.ts` with the following content:

```ts title="src/utils/custom-logger.ts"
import { Logger } from "@medusajs/framework/types"

class MyLogger implements Logger {
  panic(data: unknown): void {
    console.error("PANIC:", data)
  }
  shouldLog(level: string): boolean {
    // For demonstration, always log
    return true
  }
  setLogLevel(level: string): void {
    console.info("Set log level to:", level)
  }
  unsetLogLevel(): void {
    console.info("Unset log level")
  }
  activity(message: string, config?: unknown): string {
    console.log("ACTIVITY:", message, config)
    return "activity-id"
  }
  progress(activityId: string, message: string): void {
    console.log(`PROGRESS [${activityId}]:`, message)
  }
  error(messageOrError: string | Error, error?: Error): void {
    if (error) {
      console.error("ERROR:", messageOrError, error)
    } else {
      console.error("ERROR:", messageOrError)
    }
  }
  failure(activityId: string, message: string): unknown {
    console.warn(`FAILURE [${activityId}]:`, message)
    return null
  }
  success(activityId: string, message: string): Record<string, unknown> {
    console.log(`SUCCESS [${activityId}]:`, message)
    return { activityId, message }
  }
  silly(message: string): void {
    console.debug("SILLY:", message)
  }
  debug(message: string): void {
    console.debug("DEBUG:", message)
  }
  verbose(message: string): void {
    console.info("VERBOSE:", message)
  }
  http(message: string): void {
    console.info("HTTP:", message)
  }
  info(message: string): void {
    console.info("INFO:", message)
  }
  warn(message: string): void {
    console.warn("WARN:", message)
  }
  log(...args: unknown[]): void {
    console.log(...args)
  }
}

export const logger = new MyLogger()
```

You can implement the methods as per your requirements. For simplicity, the above example logs messages to the console with a prefix indicating the log level.

Notice that you must export an instance of your custom logger class to use it in the Medusa configurations.

You can learn more about the methods in the [Logger](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) chapter.

### Step 2: Pass the Custom Logger in Medusa Configurations

Next, to use the custom logger, set the `logger` configuration in your `medusa-config.ts` file:

```ts title="medusa-config.ts"
import { logger } from "./src/logger/my-logger"

module.exports = defineConfig({
  // ...
  logger,
})
```

The `logger` configuration accepts an instance of a class that extends the `Logger` interface.

### Test Custom Logger

To test that your custom logger is working, start the Medusa application:

```bash npm2yarn
npm run dev
```

The logs will be displayed based on your custom implementation. For example, the above implementation that logs messages to the console will show logs similar to the following:

```bash
INFO: Watching filesystem to reload dev server on file change
WARN: Local Event Bus installed. This is not recommended for production.
```


# Logging

In this chapter, you’ll learn how to use Medusa’s logging utility.

## Logger Class

Medusa provides a `Logger` class with advanced logging functionalities. This includes configuring logging levels or saving logs to a file.

By default, the `Logger` class logs messages to the console. You can also override the default logger with your custom implementation, as explained in the [Override Logger](https://docs.medusajs.com/learn/debugging-and-testing/logging/custom-logger/index.html.md) guide.

The Medusa application registers the `Logger` class in the Medusa container and each module's container as `logger`.

***

## How to Log a Message

Resolve the `logger` using the Medusa container to log a message in your resource.

For example, create the file `src/jobs/log-message.ts` with the following content:

```ts title="src/jobs/log-message.ts" highlights={highlights}
import { MedusaContainer } from "@medusajs/framework/types"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const logger = container.resolve(ContainerRegistrationKeys.LOGGER)

  logger.info("I'm using the logger!")
}

export const config = {
  name: "test-logger",
  // execute every minute
  schedule: "* * * * *",
}
```

This creates a scheduled job that resolves the `logger` from the Medusa container and uses it to log a message.

### Test the Scheduled Job

To test out the above scheduled job, start the Medusa application:

```bash npm2yarn
npm run dev
```

After a minute, you'll see the following message as part of the logged messages:

```text
info:    I'm using the logger!
```

***

## Log Levels

The `Logger` class has the following methods:

- `info`: The message is logged with level `info`.
- `warn`: The message is logged with level `warn`.
- `error`: The message is logged with level `error`.
- `debug`: The message is logged with level `debug`.

Each of these methods accepts a string parameter to log in the terminal with the associated level.

***

## Logging Configurations

### Log Level

The available log levels, from lowest to highest levels, are:

1. `silly`
2. `debug`
3. `verbose`
4. `http` (default, meaning only HTTP requests are logged)
5. `info`
6. `warn`
7. `error`

You can change that by setting the `LOG_LEVEL` environment variable to the minimum level you want to be logged.

For example:

```bash
LOG_LEVEL=error
```

This logs `error` messages only.

The environment variable must be set as a system environment variable and not in `.env`.

### Save Logs in a File

Aside from showing the logs in the terminal, you can save the logs in a file by setting the `LOG_FILE` environment variable to the path of the file relative to the Medusa server’s root directory.

For example:

```bash
LOG_FILE=all.log
```

Your logs are now saved in the `all.log` file at the root of your Medusa application.

The environment variable must be set as a system environment variable and not in `.env`.

***

## Show Log with Progress

The `Logger` class has an `activity` method used to log a message of level `info`. If the Medusa application is running in a development environment, a spinner starts to show the activity's progress.

For example:

```ts title="src/jobs/log-message.ts"
import { MedusaContainer } from "@medusajs/framework/types"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const logger = container.resolve(ContainerRegistrationKeys.LOGGER)

  const activityId = logger.activity("First log message")

  logger.progress(activityId, `Second log message`)

  logger.success(activityId, "Last log message")
}
```

The `activity` method returns the ID of the started activity. This ID can then be passed to one of the following methods of the `Logger` class:

- `progress`: Log a message of level `info` that indicates progress within that same activity.
- `success`: Log a message of level `info` that indicates that the activity has succeeded. This also ends the associated activity.
- `failure`: Log a message of level `error` that indicates that the activity has failed. This also ends the associated activity.

If you configured the `LOG_LEVEL` environment variable to a level higher than those associated with the above methods, their messages won’t be logged.


# Example: Write Integration Tests for API Routes

In this chapter, you'll learn how to write integration tests for API routes using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framework.

### Prerequisites

- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md)

## Test a GET API Route

Consider the following API route created at `src/api/custom/route.ts`:

```ts title="src/api/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
){
  res.json({
    message: "Hello, World!",
  })
}
```

To write an integration test that tests this API route, create the file `integration-tests/http/custom-routes.spec.ts` with the following content:

```ts title="integration-tests/http/custom-routes.spec.ts" highlights={getHighlights}
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      describe("GET /custom", () => {
        it("returns correct message", async () => {
          const response = await api.get(
            `/custom`
          )
  
          expect(response.status).toEqual(200)
          expect(response.data).toHaveProperty("message")
          expect(response.data.message).toEqual("Hello, World!")
        })
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

You use the `medusaIntegrationTestRunner` to write your tests.

You add a single test that sends a `GET` request to `/custom` using the `api.get` method. For the test to pass, the response is expected to:

- Have a code status `200`,
- Have a `message` property in the returned data.
- Have the value of the `message` property equal to `Hello, World!`.

### Run Tests

Run the following command to run your tests:

```bash npm2yarn
npm run test:integration:http
```

If you don't have a `test:integration:http` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md).

This runs your Medusa application and runs the tests available under the `src/integrations/http` directory.

### Jest Timeout

Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test:

```ts title="integration-tests/http/custom-routes.spec.ts"
// in your test's file
jest.setTimeout(60 * 1000)
```

***

## Test a POST API Route

Suppose you have a `blog` module whose main service extends the service factory, and that has the following model:

```ts title="src/modules/blog/models/my-custom.ts"
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id().primaryKey(),
  name: model.text(),
})

export default Post
```

And consider that the file `src/api/custom/route.ts` defines another route handler for `POST` requests:

```ts title="src/api/custom/route.ts"
// other imports...
import BlogModuleService from "../../../modules/blog/service"
import { BLOG_MODULE } from "../../../modules/blog"

// ...

export async function POST(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const blogModuleService: BlogModuleService = req.scope.resolve(
    BLOG_MODULE
  )

  const post = await blogModuleService.createPosts(
    req.body
  )

  res.json({
    post,
  })
}
```

This API route creates a new record of `Post`.

To write tests for this API route, add the following at the end of the `testSuite` function in `integration-tests/http/custom-routes.spec.ts`:

```ts title="integration-tests/http/custom-routes.spec.ts" highlights={postHighlights}
// other imports...
import BlogModuleService from "../../src/modules/blog/service"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      // other tests...

      describe("POST /custom", () => {
        const id = "1"

        it("Creates my custom", async () => {
  
          const response = await api.post(
            `/custom`,
            {
              id,
              name: "Test",
            }
          )
  
          expect(response.status).toEqual(200)
          expect(response.data).toHaveProperty("post")
          expect(response.data.post).toEqual({
            id,
            name: "Test",
            created_at: expect.any(String),
            updated_at: expect.any(String),
          })
        })
      })
    })
  },
})
```

This adds a test for the `POST /custom` API route. It uses `api.post` to send the POST request. The `api.post` method accepts as a second parameter the data to pass in the request body.

The test passes if the response has:

- Status code `200`.
- A `post` property in its data.
- Its `id` and `name` match the ones provided to the request.

### Tear Down Created Record

To ensure consistency in the database for the rest of the tests after the above test is executed, utilize [Jest's setup and teardown hooks](https://jestjs.io/docs/setup-teardown) to delete the created record.

Use the `getContainer` function passed as a parameter to the `testSuite` function to resolve a service and use it for setup or teardown purposes

So, add an `afterAll` hook in the `describe` block for `POST /custom`:

```ts title="integration-tests/http/custom-routes.spec.ts"
// other imports...
import BlogModuleService from "../../src/modules/blog/service"
import { BLOG_MODULE } from "../../src/modules/blog"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      // other tests...

      describe("POST /custom", () => {
        // ...
        afterAll(() => async () => {
          const blogModuleService: BlogModuleService = getContainer().resolve(
            BLOG_MODULE
          )

          await blogModuleService.deletePosts(id)
        })
      })
    })
  },
})
```

The `afterAll` hook resolves the `BlogModuleService` and use its `deletePosts` to delete the record created by the test.

***

## Test a DELETE API Route

Consider a `/custom/:id` API route created at `src/api/custom/[id]/route.ts`:

```ts title="src/api/custom/[id]/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import BlogModuleService from "../../../modules/blog/service"
import { BLOG_MODULE } from "../../../modules/blog"

export async function DELETE(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const blogModuleService: BlogModuleService = req.scope.resolve(
    BLOG_MODULE
  )

  await blogModuleService.deletePosts(req.params.id)

  res.json({
    success: true,
  })
}
```

This API route accepts an ID path parameter, and uses the `BlogModuleService` to delete a `Post` record by that ID.

To add tests for this API route, add the following to `integration-tests/http/custom-routes.spec.ts`:

```ts title="integration-tests/http/custom-routes.spec.ts" highlights={deleteHighlights}
medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      // ...

      describe("DELETE /custom/:id", () => {
        const id = "1"

        beforeAll(() => async () => {
          const blogModuleService: BlogModuleService = getContainer().resolve(
            BLOG_MODULE
          )

          await blogModuleService.createPosts({
            id,
            name: "Test",
          })
        })

        it("Deletes my custom", async () => {
          const response = await api.delete(
            `/custom/${id}`
          )

          expect(response.status).toEqual(200)
          expect(response.data).toHaveProperty("success")
          expect(response.data.success).toBeTruthy()
        })
      })
    })
  },
})
```

This adds a new test for the `DELETE /custom/:id` API route. You use the `beforeAll` hook to create a `Post` record using the `BlogModuleService`.

In the test, you use the `api.delete` method to send a `DELETE` request to `/custom/:id`. The test passes if the response:

- Has a `200` status code.
- Has a `success` property in its data.
- The `success` property's value is true.

***

## Pass Headers in Test Requests

Some requests require passing headers. For example, all routes prefixed with `/store` must pass a publishable API key in the header.

The `get`, `post`, and `delete` methods accept an optional third parameter that you can pass a `headers` property to, whose value is an object of headers to pass in the request.

### Pass Publishable API Key

For example, to pass a publishable API key in the header for a request to a `/store` route:

```ts title="integration-tests/http/custom-routes.spec.ts" highlights={headersHighlights}
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { ApiKeyDTO } from "@medusajs/framework/types"
import { createApiKeysWorkflow } from "@medusajs/medusa/core-flows"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      let pak: ApiKeyDTO
      beforeAll(async () => {
        pak = (await createApiKeysWorkflow(getContainer()).run({
          input: {
            api_keys: [
              {
                type: "publishable",
                title: "Test Key",
                created_by: "",
              },
            ],
          },
        })).result[0]
      })
      describe("GET /custom", () => {
        it("returns correct message", async () => {
          const response = await api.get(
            `/store/custom`,
            {
              headers: {
                "x-publishable-api-key": pak.token,
              },
            }
          )
  
          expect(response.status).toEqual(200)
          expect(response.data).toHaveProperty("message")
          expect(response.data.message).toEqual("Hello, World!")
        })
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

In your test suit, you add a `beforeAll` hook to create a publishable API key before the tests run. To create the API key, you can use the `createApiKeysWorkflow` or the [API Key Module's service](https://docs.medusajs.com/resources/commerce-modules/api-key/index.html.md).

Then, in the test, you pass an object as the last parameter to `api.get` with a `headers` property. The `headers` property is an object with the key `x-publishable-api-key` and the value of the API key's token.

### Send Authenticated Requests

If your custom route is accessible by authenticated users only, such as routes prefixed by `/admin` or `/store/customers/me`, you can create a test customer or user, generate a JWT token for them, and pass the token in the request's Authorization header.

For example:

- The `jsonwebtoken` is available in your application by default.
- For custom actor types, you only need to change the `actorType` value in the `jwt.sign` method.

### Admin User

```ts title="integration-tests/http/custom-routes.spec.ts" highlights={adminHighlights}
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import jwt from "jsonwebtoken"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      describe("GET /custom", () => {
        const headers: Record<string, string> = {
        }
        beforeEach(async () => {
          const container = getContainer()
          
          const authModuleService = container.resolve("auth")
          const userModuleService = container.resolve("user")
          
          const user = await userModuleService.createUsers({
            email: "admin@medusa.js",
            
          })
          const authIdentity = await authModuleService.createAuthIdentities({
            provider_identities: [
              {
                provider: "emailpass",
                entity_id: "admin@medusa.js",
                provider_metadata: {
                  password: "supersecret",
                },
              },
            ],
            app_metadata: {
              user_id: user.id,
            },
          })
  
          const token = jwt.sign(
            {
              actor_id: user.id,
              actor_type: "user",
              auth_identity_id: authIdentity.id,
            },
            "supersecret",
            {
              expiresIn: "1d",
            }
          )
        
          headers["authorization"] = `Bearer ${token}`
        })
        it("returns correct message", async () => {
          const response = await api.get(
            `/admin/custom`,
            { headers }
          )
  
          expect(response.status).toEqual(200)
        })
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

### Customer User

```ts title="integration-tests/http/custom-routes.spec.ts" highlights={customerHighlights}
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { ApiKeyDTO } from "@medusajs/framework/types"
import jwt from "jsonwebtoken"
import { createApiKeysWorkflow } from "@medusajs/medusa/core-flows"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      describe("GET /custom", () => {
        const headers: Record<string, string> = {
        }
        beforeEach(async () => {
          const container = getContainer()
          
          const authModuleService = container.resolve("auth")
          const customerModuleService = container.resolve("customer")
          
          const customer = await customerModuleService.createCustomers({
            email: "admin@medusa.js",
            
          })
          const authIdentity = await authModuleService.createAuthIdentities({
            provider_identities: [
              {
                provider: "emailpass",
                entity_id: "customer@medusa.js",
                provider_metadata: {
                  password: "supersecret",
                },
              },
            ],
            app_metadata: {
              user_id: customer.id,
            },
          })
  
          const token = jwt.sign(
            {
              actor_id: customer.id,
              actor_type: "customer",
              auth_identity_id: authIdentity.id,
            },
            "supersecret",
            {
              expiresIn: "1d",
            }
          )
        
          headers["authorization"] = `Bearer ${token}`


          const pak = (await createApiKeysWorkflow(getContainer()).run({
            input: {
              api_keys: [
                {
                  type: "publishable",
                  title: "Test Key",
                  created_by: "",
                },
              ],
            },
          })).result[0]

          headers["x-publishable-api-key"] = pak.token
        })
        it("returns correct message", async () => {
          const response = await api.get(
            `/store/customers/me/custom`,
            { headers }
          )
  
          expect(response.status).toEqual(200)
        })
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

In the test suite, you add a `beforeEach` hook that creates a user or customer, an auth identity, and generates a JWT token for them. The JWT token is then set in the `Authorization` header of the request.

You also create and pass a publishable API key in the header for the customer as it's required for requests to `/store` routes. Learn more in [this section](#pass-publishable-api-key).

***

## Upload Files in Test Requests

If your API route requires uploading a file, create a `FormData` object imported from the `form-data` object, then pass the form data headers in the request.

For example:

The `form-data` package is available by default.

```ts title="integration-tests/http/custom-routes.spec.ts"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import FormData from "form-data"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      describe("GET /custom", () => {
        it("upload file", async () => {
          const form = new FormData()
          form.append("files", Buffer.from("content 1"), "image1.jpg")
          form.append("files", Buffer.from("content 2"), "image2.jpg")

          const response = await api.post(`/custom`, form, {
            headers: form.getHeaders(),
          })
  
          expect(response.status).toEqual(200)
          expect(response.data).toHaveProperty("files")
          expect(response.data.files).toEqual(
            expect.arrayContaining([
              expect.objectContaining({
                id: expect.any(String),
                url: expect.any(String),
              }),
            ])
          )
        })
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

You don't have to actually upload a file, you use the `form.append` method to append to a `files` field in the form data object, and you pass random content using the `Buffer.from` method.

Then, you pass to the `api.post` method the form data object as a second parameter, and an object with the `headers` property set to the form data object's headers as a third parameter.

If you're passing authentication or other headers, you can pass both the form data headers and the authentication headers in the same object:

```ts title="integration-tests/http/custom-routes.spec.ts"
const response = await api.post(`/custom`, form, {
  headers: {
    ...form.getHeaders(),
    ...authHeaders,
  },
})
```

***

## Write Tests for Feature-Flagged API Routes

If your API route is hidden behind a \[feature flag], you can use the `env` option of `medusaIntegrationTestRunner` its feature flag.

For example:

```ts title="integration-tests/http/custom-routes.spec.ts"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"

medusaIntegrationTestRunner({
  env: {
    FF_BLOG_FEATURE: true,
  },
  testSuite: ({ api, getContainer }) => {
    // TODO write tests...
  },
})
```

The `env` option accepts an object of environment variables to set for the test suite.


# Write Integration Tests

In this chapter, you'll learn about `medusaIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests.

### Prerequisites

- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md)

## medusaIntegrationTestRunner Utility

The `medusaIntegrationTestRunner` is from Medusa's Testing Framework and it's used to create integration tests in your Medusa project. It runs a full Medusa application, allowing you test API routes, workflows, or other customizations.

For example:

```ts title="integration-tests/http/test.spec.ts" highlights={highlights}
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    // TODO write tests...
  },
})

jest.setTimeout(60 * 1000)
```

The `medusaIntegrationTestRunner` function accepts an object as a parameter. The object has a required property `testSuite`.

`testSuite`'s value is a function that defines the tests to run. The function accepts as a parameter an object that has the following properties:

- `api`: a set of utility methods used to send requests to the Medusa application. It has the following methods:
  - `get`: Send a `GET` request to an API route.
  - `post`: Send a `POST` request to an API route.
  - `delete`: Send a `DELETE` request to an API route.
- `getContainer`: a function that retrieves the Medusa Container. Use the `getContainer().resolve` method to resolve resources from the Medusa Container.

The tests in the `testSuite` function are written using [Jest](https://jestjs.io/).

### Jest Timeout

Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test:

```ts title="integration-tests/http/test.spec.ts"
// in your test's file
jest.setTimeout(60 * 1000)
```

***

### Run Tests

Run the following command to run your tests:

```bash npm2yarn
npm run test:integration:http
```

If you don't have a `test:integration:http` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md).

This runs your Medusa application and runs the tests available under the `src/integrations/http` directory.

***

## Other Options and Inputs

Refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function.

***

## Database Used in Tests

The `medusaIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end.

To manage that database, such as changing its name or perform operations on it in your tests, refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md).

***

## Example Integration Tests

The next chapters provide examples of writing integration tests for API routes and workflows.


# Example: Write Integration Tests for Workflows

In this chapter, you'll learn how to write integration tests for workflows using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framework.

For other debugging approaches, refer to the [Debug Workflows](https://docs.medusajs.com/learn/debugging-and-testing/debug-workflows/index.html.md) chapter.

### Prerequisites

- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md)

## Write Integration Test for a Workflow

Consider you have the following workflow defined at `src/workflows/hello-world.ts`:

```ts title="src/workflows/hello-world.ts"
import {
  createWorkflow,
  createStep,
  StepResponse,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep("step-1", () => {
  return new StepResponse("Hello, World!")
})

export const helloWorldWorkflow = createWorkflow(
  "hello-world-workflow",
  () => {
    const message = step1()

    return new WorkflowResponse(message)
  }
)
```

To write a test for this workflow, create the file `integration-tests/http/workflow.spec.ts` with the following content:

```ts title="integration-tests/http/workflow.spec.ts"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { helloWorldWorkflow } from "../../src/workflows/hello-world"

medusaIntegrationTestRunner({
  testSuite: ({ getContainer }) => {
    describe("Test hello-world workflow", () => {
      it("returns message", async () => {
        const { result } = await helloWorldWorkflow(getContainer())
          .run()

        expect(result).toEqual("Hello, World!")
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

You use the `medusaIntegrationTestRunner` to write an integration test for the workflow. The test passes if the workflow returns the string `"Hello, World!"`.

### Jest Timeout

Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test:

```ts title="integration-tests/http/custom-routes.spec.ts"
// in your test's file
jest.setTimeout(60 * 1000)
```

***

## Run Tests

Run the following command to run your tests:

```bash npm2yarn
npm run test:integration:http
```

If you don't have a `test:integration:http` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md).

This runs your Medusa application and runs the tests available under the `integration-tests/http` directory.

***

## Test That a Workflow Throws an Error

You might want to verify that a workflow throws an error in certain edge cases. To test that a workflow throws an error:

- Disable the `throwOnError` option when executing the workflow.
- Use the returned `errors` property to check what errors were thrown.

For example, if you have the following step in your workflow that throws a `MedusaError`:

```ts title="src/workflows/hello-world.ts"
import { MedusaError } from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk"

const step1 = createStep("step-1", () => {
  throw new MedusaError(MedusaError.Types.NOT_FOUND, "Item doesn't exist")
})
```

You can write the following test to ensure that the workflow throws that error:

```ts title="integration-tests/http/workflow.spec.ts"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { helloWorldWorkflow } from "../../src/workflows/hello-world"

medusaIntegrationTestRunner({
  testSuite: ({ getContainer }) => {
    describe("Test hello-world workflow", () => {
      it("should throw error when item doesn't exist", async () => {
        const { errors } = await helloWorldWorkflow(getContainer())
          .run({
            throwOnError: false,
          })

        expect(errors.length).toBeGreaterThan(0)
        expect(errors[0].error.message).toBe("Item doesn't exist")
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

The `errors` property contains an array of errors thrown during the execution of the workflow. Each error item has an `error` object, which is the error thrown.

If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`.

***

## Test Long-Running Workflows

Since [long-running workflows](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md) run asynchronously, testing them requires a different approach than synchronous workflows.

When testing long-running workflows, you need to:

1. Set the asynchronous steps as successful manually.
2. Subscribe to the workflow's events to listen for the workflow execution's completion.
3. Verify the output of the workflow after it has completed.

For example, consider you have the following long-running workflow defined at `src/workflows/long-running-workflow.ts`:

```ts title="src/workflows/long-running-workflow.ts" highlights={[["15"]]}
import { 
  createStep,  
  createWorkflow,
  WorkflowResponse,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep("step-1", async () => {
  return new StepResponse({})
})

const step2 = createStep(
  {
    name: "step-2",
    async: true,
  },
  async () => {
    console.log("Waiting to be successful...")
  }
)

const step3 = createStep("step-3", async () => {
  return new StepResponse("Finished three steps")
})

const longRunningWorkflow = createWorkflow(
  "long-running", 
  function () {
    step1()
    step2()
    const message = step3()

    return new WorkflowResponse({
      message,
    })
  }
)

export default longRunningWorkflow
```

`step2` in this workflow is an asynchronous step that you need to set as successful manually in your test.

You can write the following test to ensure that the long-running workflow completes successfully:

```ts title="integration-tests/http/long-running-workflow.spec.ts" highlights={longRunningWorkflowHighlights1}
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import longRunningWorkflow from "../../src/workflows/long-running-workflow"
import { Modules, TransactionHandlerType } from "@medusajs/framework/utils"
import { StepResponse } from "@medusajs/framework/workflows-sdk"

medusaIntegrationTestRunner({
  testSuite: ({ getContainer }) => {
    describe("Test long-running workflow", () => {
      it("returns message", async () => {
        const container = getContainer()
        const { transaction } = await longRunningWorkflow(container)
          .run()

        const workflowEngineService = container.resolve(
          Modules.WORKFLOW_ENGINE
        )

        let workflowOk: any
        const workflowCompletion = new Promise((ok) => {
          workflowOk = ok
        })

        const subscriptionOptions = {
          workflowId: "long-running",
          transactionId: transaction.transactionId,
          subscriberId: "long-running-subscriber",
        }


        await workflowEngineService.subscribe({
          ...subscriptionOptions,
          subscriber: async (data) => {
            if (data.eventType === "onFinish") {
              workflowOk(data.result.message)
              // unsubscribe
              await workflowEngineService.unsubscribe({
                ...subscriptionOptions,
                subscriberOrId: subscriptionOptions.subscriberId,
              })
            }
          },
        })

        await workflowEngineService.setStepSuccess({
          idempotencyKey: {
            action: TransactionHandlerType.INVOKE,
            transactionId: transaction.transactionId,
            stepId: "step-2",
            workflowId: "long-running",
          },
          stepResponse: new StepResponse("Done!"),
        })

        const afterSubscriber = await workflowCompletion

        expect(afterSubscriber).toBe("Finished three steps")
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

In this test, you:

1. Execute the long-running workflow and get the transaction details from the `run` method's result.
2. Resolve the [Workflow Engine Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/index.html.md)'s service from the Medusa container.
3. Create a promise to wait for the workflow's completion.
4. Subscribe to the workflow's events using the Workflow Engine Module's `subscribe` method.
   - The `subscriber` function is called whenever an event related to the workflow occurs. On the `onFinish` event that indicates the workflow has completed, you resolve the promise with the workflow's result.
5. Set the asynchronous step as successful using the `setStepSuccess` method of the Workflow Engine Module.
6. Wait for the promise to resolve, which indicates that the workflow has completed successfully.
7. Finally, you assert that the workflow's result matches the expected output.

If you run the integration test, it will execute the long-running workflow and verify that it completes and returns the expected result.

### Example with Multiple Asynchronous Steps

If your long-running workflow has multiple asynchronous steps, you must set each of them as successful in your test before the workflow can complete.

Here's how the test would look like if you had two asynchronous steps:

```ts title="integration-tests/http/long-running-workflow-multiple-steps.spec.ts" highlights={longRunningWorkflowHighlights2}
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import longRunningWorkflow from "../../src/workflows/long-running-workflow"
import { Modules, TransactionHandlerType } from "@medusajs/framework/utils"
import { StepResponse } from "@medusajs/framework/workflows-sdk"

medusaIntegrationTestRunner({
  testSuite: ({ getContainer }) => {
    describe("Test long-running workflow with multiple async steps", () => {
      it("returns message", async () => {
        const container = getContainer()
        const { transaction } = await longRunningWorkflow(container)
          .run()

        const workflowEngineService = container.resolve(
          Modules.WORKFLOW_ENGINE
        )

        let workflowOk: any
        const workflowCompletion = new Promise((ok) => {
          workflowOk = ok
        })

        const subscriptionOptions = {
          workflowId: "long-running",
          transactionId: transaction.transactionId,
          subscriberId: "long-running-subscriber",
        }

        await workflowEngineService.subscribe({
          ...subscriptionOptions,
          subscriber: async (data) => {
            if (data.eventType === "onFinish") {
              workflowOk(data.result.message)
              // unsubscribe
              await workflowEngineService.unsubscribe({
                ...subscriptionOptions,
                subscriberOrId: subscriptionOptions.subscriberId,
              })
            }
          },
        })

        await workflowEngineService.setStepSuccess({
          idempotencyKey: {
            action: TransactionHandlerType.INVOKE,
            transactionId: transaction.transactionId,
            stepId: "step-2",
            workflowId: "long-running",
          },
          stepResponse: new StepResponse("Done!"),
        })

        await workflowEngineService.setStepSuccess({
          idempotencyKey: {
            action: TransactionHandlerType.INVOKE,
            transactionId: transaction.transactionId,
            stepId: "step-3",
            workflowId: "long-running",
          },
          stepResponse: new StepResponse("Done with step 3!"),
        })

        const afterSubscriber = await workflowCompletion

        expect(afterSubscriber).toBe("Finished three steps")
      })
    })
  },
})
```

In this example, you set both `step-2` and `step-3` as successful before waiting for the workflow to complete.

***

## Test Database Operations in Workflows

In real use cases, you'll often test workflows that perform database operations, such as creating a brand.

When you test such workflows, you may need to:

- Verify that the database operations were performed correctly. For example, that a brand was created with the expected properties.
- Perform database actions before testing the workflow. For example, creating a brand before testing a workflow that deletes it.

This section provides examples of both scenarios.

### Verify Database Operations in Workflow Test

To retrieve data from the database after running a workflow, you can resolve and use either the module's service (for example, the Brand Module's service) or [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md).

For example, the following test verifies that a brand was created by a workflow:

```ts title="integration-tests/http/workflow-brand.spec.ts" highlights={workflowBrandHighlights}
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { createBrandWorkflow } from "../../src/workflows/create-brand"
import { BRAND_MODULE } from "../../src/modules/brand"

medusaIntegrationTestRunner({
  testSuite: ({ getContainer }) => {
    describe("Test create brand workflow", () => {
      it("creates a brand", async () => {
        const container = getContainer()
        const { result: brand } = await createBrandWorkflow(container)
          .run({
            input: {
              name: "Test Brand",
            },
          })

        const brandModuleService = container.resolve(BRAND_MODULE)

        const createdBrand = await brandModuleService.retrieveBrand(brand.id)
        expect(createdBrand).toBeDefined()
        expect(createdBrand.name).toBe("Test Brand")
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

In this test, you run the workflow, which creates a brand. Then, you retrieve the brand from the database using the Brand Module's service and verify that it was created with the expected properties.

### Perform Database Actions Before Testing Workflow

You can perform database actions before testing workflows in the `beforeAll` or `beforeEach` hooks of your test suite. In those hooks, you can create data that is useful for your workflow tests.

Learn more about test hooks in [Jest's Documentation](https://jestjs.io/docs/setup-teardown).

You can perform the database actions before testing a workflow by either:

- Using the module's service (for example, the Brand Module's service).
- Using an existing workflow that performs the database actions.

#### Use Module's Service

For example, the following test creates a brand using the Brand Module's service before running the workflow that deletes it:

```ts title="integration-tests/http/workflow-brand-delete.spec.ts"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { deleteBrandWorkflow } from "../../src/workflows/delete-brand"
import { BRAND_MODULE } from "../../src/modules/brand"

medusaIntegrationTestRunner({
  testSuite: ({ getContainer }) => {
    let brandId: string

    beforeAll(async () => {
      const container = getContainer()
      
      const brandModuleService = container.resolve(BRAND_MODULE)
      
      const brand = await brandModuleService.createBrands({
        name: "Test Brand",
      })

      brandId = brand.id
    })

    describe("Test delete brand workflow", () => {
      it("deletes a brand", async () => {
        const container = getContainer()
        const { result } = await deleteBrandWorkflow(container)
          .run({
            input: {
              id: brandId,
            },
          })

        expect(result.success).toBe(true)

        const brandModuleService = container.resolve(BRAND_MODULE)
        await expect(brandModuleService.retrieveBrand(brandId))
          .rejects.toThrow()
      })
    })
  },
})
```

In this example, you:

1. Use the `beforeAll` hook to create a brand before running the workflow that deletes it.
2. Create a test that runs the `deleteBrandWorkflow` to delete the created brand.
3. Verify that the brand was deleted successfully by checking that retrieving it throws an error.

#### Use Existing Workflow

Alternatively, if you already have a workflow that performs the database operations, you can use that workflow in the `beforeAll` or `beforeEach` hook. This is useful if the database operations are complex and are already encapsulated in a workflow.

For example, you can modify the `beforeAll` hook to use the `createBrandWorkflow`:

```ts title="integration-tests/http/workflow-brand-delete.spec.ts" highlights={workflowBrandDeleteHighlights2}
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { deleteBrandWorkflow } from "../../src/workflows/delete-brand"
import { createBrandWorkflow } from "../../src/workflows/create-brand"
import { BRAND_MODULE } from "../../src/modules/brand"

medusaIntegrationTestRunner({
  testSuite: ({ getContainer }) => {
    let brandId: string

    beforeAll(async () => {
      const container = getContainer()
      
      const { result: brand } = await createBrandWorkflow(container)
        .run({
          input: {
            name: "Test Brand",
          },
        })

      brandId = brand.id
    })

    describe("Test delete brand workflow", () => {
      it("deletes a brand", async () => {
        const container = getContainer()
        const { result } = await deleteBrandWorkflow(container)
          .run({
            input: {
              id: brandId,
            },
          })

        expect(result.success).toBe(true)

        const brandModuleService = container.resolve(BRAND_MODULE)
        await expect(brandModuleService.retrieveBrand(brandId))
          .rejects.toThrow()
      })
    })
  },
})
```

In this example, you:

1. Use the `beforeAll` hook to run the `createBrandWorkflow`, which creates a brand before running the workflow that deletes it.
2. Create a test that runs the `deleteBrandWorkflow` to delete the created brand.
3. Verify that the brand was deleted successfully by checking that retrieving it throws an error.


# Write Tests for Modules

In this chapter, you'll learn about `moduleIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests for a module's main service.

### Prerequisites

- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md)

## moduleIntegrationTestRunner Utility

`moduleIntegrationTestRunner` creates integration tests for a module's service. The integration tests run on a test Medusa application with only the specified module enabled.

For example, consider a Blog Module with a `BlogModuleService` that has a `getMessage` method:

```ts title="src/modules/blog/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

class BlogModuleService extends MedusaService({
  Post,
}){
  async getMessage(): Promise<string> {
    return "Hello, World!"
  }
}

export default BlogModuleService
```

To create an integration test for the module's service, create the file `src/modules/blog/__tests__/service.spec.ts` with the following content:

```ts title="src/modules/blog/__tests__/service.spec.ts"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { BLOG_MODULE } from ".."
import BlogModuleService from "../service"
import Post from "../models/post"

moduleIntegrationTestRunner<BlogModuleService>({
  moduleName: BLOG_MODULE,
  moduleModels: [Post],
  resolve: "./src/modules/blog",
  testSuite: ({ service }) => {
    describe("BlogModuleService", () => {
      it("says hello world", () => {
        const message = service.getMessage()

        expect(message).toEqual("Hello, World!")
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

The `moduleIntegrationTestRunner` function accepts as a parameter an object with the following properties:

- `moduleName`: The registration name of the module.
- `moduleModels`: An array of models in the module. Refer to [this section](#write-tests-for-modules-without-data-models) if your module doesn't have data models.
- `resolve`: The path to the module's directory.
- `testSuite`: A function that defines [Jest](https://jestjs.io/) tests to run.

The `testSuite` function accepts as a parameter an object having the `service` property, which is an instance of the module's main service.

The type argument provided to the `moduleIntegrationTestRunner` function is used as the type of the `service` property.

The tests in the `testSuite` function are written using [Jest](https://jestjs.io/).

***

## Run Tests

Run the following command to run your module integration tests:

```bash npm2yarn
npm run test:integration:modules
```

If you don't have a `test:integration:modules` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md).

This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory.

***

## Pass Module Options

If your module accepts options, you can set them using the `moduleOptions` property of the `moduleIntegrationTestRunner`'s parameter.

For example:

```ts
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import BlogModuleService from "../service"

moduleIntegrationTestRunner<BlogModuleService>({
  moduleOptions: {
    apiKey: "123",
  },
  // ...
})
```

`moduleOptions` is an object of key-value pair options that your module's service receives in its constructor.

***

## Write Tests for Modules without Data Models

If your module doesn't have a data model, pass a dummy model in the `moduleModels` property.

For example:

```ts
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import BlogModuleService from "../service"
import { model } from "@medusajs/framework/utils"

const DummyModel = model.define("dummy_model", {
  id: model.id().primaryKey(),
})

moduleIntegrationTestRunner<BlogModuleService>({
  moduleModels: [DummyModel],
  // ...
})

jest.setTimeout(60 * 1000)
```

***

## Inject Dependencies in Module Tests

Some modules have injected dependencies, such as the [Event Module's service](https://docs.medusajs.com/resources/infrastructure-modules/event/index.html.md). When writing tests for those modules, you need to inject the dependencies that the module's service requires to avoid errors.

You can inject dependencies as mock dependencies that simulate the behavior of the original service. This way you avoid unexpected behavior or results, such as sending real events or making real API calls.

To inject dependencies, pass the `injectedDependencies` property to the `moduleIntegrationTestRunner` function.

For example:

```ts title="src/modules/blog/__tests__/service.spec.ts" highlights={mockDependenciesHighlights}
import { MockEventBusService, moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { BLOG_MODULE } from ".."
import BlogModuleService from "../service"
import Post from "../models/post"
import { Modules } from "@medusajs/framework/utils"

moduleIntegrationTestRunner<BlogModuleService>({
  moduleName: BLOG_MODULE,
  moduleModels: [Post],
  resolve: "./src/modules/blog",
  injectedDependencies: {
    [Modules.EVENT_BUS]: new MockEventBusService(),
  },
  testSuite: ({ service }) => {
    describe("BlogModuleService", () => {
      it("says hello world", async () => {
        const message = await service.getMessage()

        expect(message).toEqual("Hello, World!")
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

`injectedDependencies`'s value is an object whose keys are registration names of the dependencies you want to inject, and the values are the mock services.

In this example, you inject a mock Event Module service into the `BlogModuleService`. Medusa exposes a `MockEventBusService` class that you can use to mock the Event Module's service.

For other modules, you can create a mock service that implements the same interface as the original service. Make sure to use the same registration name as the original service when injecting it.

***

### Other Options and Inputs

Refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function.

***

## Database Used in Tests

The `moduleIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end.

To manage that database, such as changing its name or perform operations on it in your tests, refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md).


# Medusa Testing Tools

In this chapter, you'll learn about Medusa's testing tools and how to install and configure them.

## @medusajs/test-utils Package

Medusa provides a Testing Framework to create integration tests for your custom API routes, modules, or other Medusa customizations.

To use the Testing Framework, install `@medusajs/test-utils` as a `devDependency`:

```bash npm2yarn
npm install --save-dev @medusajs/test-utils@latest
```

***

## Install and Configure Jest

Writing tests with `@medusajs/test-utils`'s tools requires installing and configuring Jest in your project.

Run the following command to install the required Jest dependencies:

```bash npm2yarn
npm install --save-dev jest @types/jest @swc/jest
```

Then, create the file `jest.config.js` with the following content:

```js title="jest.config.js"
const { loadEnv } = require("@medusajs/framework/utils")
loadEnv("test", process.cwd())

module.exports = {
  transform: {
    "^.+\\.[jt]s$": [
      "@swc/jest",
      {
        jsc: {
          parser: { syntax: "typescript", decorators: true },
          target: "es2021",
        },
      },
    ],
  },
  testEnvironment: "node",
  moduleFileExtensions: ["js", "ts", "json"],
  modulePathIgnorePatterns: ["dist/"],
  setupFiles: ["./integration-tests/setup.js"],
}

if (process.env.TEST_TYPE === "integration:http") {
  module.exports.testMatch = ["**/integration-tests/http/*.spec.[jt]s"]
} else if (process.env.TEST_TYPE === "integration:modules") {
  module.exports.testMatch = ["**/src/modules/*/__tests__/**/*.[jt]s"]
} else if (process.env.TEST_TYPE === "unit") {
  module.exports.testMatch = ["**/src/**/__tests__/**/*.unit.spec.[jt]s"]
}
```

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("@medusajs/framework/mikro-orm/core")

MetadataStorage.clear()
```

***

## Add Test Commands

Finally, add the following scripts to `package.json`:

```json title="package.json"
"scripts": {
  // ...
  "test:integration:http": "TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit",
  "test:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit",
  "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit"
},
```

You now have two commands:

- `test:integration:http` to run integration tests (for example, for API routes and workflows) available under the `integration-tests/http` directory.
- `test:integration:modules` to run integration tests for modules available in any `__tests__` directory under `src/modules`.
- `test:unit` to run unit tests in any `__tests__` directory under the `src` directory.

Medusa's Testing Framework works for integration tests only. You can write unit tests using Jest.

***

## Test Tools and Writing Tests

The next chapters explain how to use the testing tools provided by `@medusajs/test-utils` to write tests.


# General Medusa Application Deployment Guide

In this guide, you'll learn the general steps to deploy your Medusa application. How you apply these steps depend on your chosen hosting provider or platform.

Before deciding to self-host your Medusa application, consider reading the [Cloud vs. Self-Hosting comparison guide](https://docs.medusajs.com/cloud/comparison/index.html.md). By using Cloud, you can avoid the complexities and challenges of self-hosting, while benefiting from automated deployment, scaling, and maintenance.

With Cloud, you also maintain full customization control as you deploy your own modules and customizations directly from GitHub:

- Push to deploy.
- Multiple testing environments.
- Preview environments for new PRs.
- Test on production-like data.
- Storefront deployment.
- Emails setup.
- [And more](https://docs.medusajs.com/cloud#cloud-features/index.html.md).

### Prerequisites

- [Medusa application’s codebase hosted on GitHub repository.](https://docs.medusajs.com/learn/index.html.md)

## What You'll Deploy

When you deploy the Medusa application, you need to deploy the following resources:

1. PostgreSQL database: This is the database that will hold your Medusa application's details.
2. Redis database: This is the database that will store the Medusa server's session.
3. Medusa application in [server and worker mode](https://docs.medusajs.com/learn/production/worker-mode/index.html.md), where:
   - The server mode handles incoming API requests and serving the Medusa Admin dashboard.
   - The worker mode handles background tasks, such as scheduled jobs and subscribers.

So, when choosing a hosting provider, make sure it supports deploying these resources. Also, for optimal experience, the hosting provider and plan must offer at least 2GB of RAM.

***

## 1. Configure Medusa Application

### Worker Mode

The `workerMode` configuration determines which mode the Medusa application runs in. When you deploy the Medusa application, you deploy two instances: one in server mode, and one in worker mode.

Learn more about worker mode in the [Worker Module chapter](https://docs.medusajs.com/learn/production/worker-mode/index.html.md).

So, add the following configuration in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    // ...
    workerMode: process.env.MEDUSA_WORKER_MODE as "shared" | "worker" | "server",
  },
})
```

Later, you’ll set different values of the `MEDUSA_WORKER_MODE` environment variable for each Medusa application deployment or instance.

### Configure Medusa Admin

The Medusa Admin is served by the Medusa server application. So, you need to disable it in the worker Medusa application only.

To disable the Medusa Admin in the worker Medusa application while keeping it enabled in the server Medusa application, add the following configuration in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  admin: {
    disable: process.env.DISABLE_MEDUSA_ADMIN === "true",
  },
})
```

Later, you’ll set different values of the `DISABLE_MEDUSA_ADMIN` environment variable for each Medusa application instance.

### Configure Redis URL

The `redisUrl` configuration specifies the connection URL to Redis to store the Medusa server's session.

Learn more in the [Medusa Configuration documentation](https://docs.medusajs.com/learn/configurations/medusa-config#redisurl/index.html.md).

So, add the following configuration in `medusa-config.ts` :

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    // ...
    redisUrl: process.env.REDIS_URL,
  },
})
```

***

## 2. Add predeploy Script

Before you start the Medusa application in production, you should always run migrations and sync links.

So, add the following script in `package.json`:

```json
"scripts": {
  // ...
  "predeploy": "medusa db:migrate"
},
```

***

## 3. Install Production Modules and Providers

By default, your Medusa application uses modules and providers useful for development, such as the Local File Module Provider.

It’s highly recommended to instead use modules and providers suitable for production, including:

- [Redis Caching Module](https://docs.medusajs.com/resources/infrastructure-modules/caching/providers/redis/index.html.md)
- [Redis Event Bus Module](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md)
- [Workflow Engine Redis Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md)
- [Redis Locking Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/locking/redis/index.html.md)
- [PostHog Analytics Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/analytics/posthog/index.html.md) (If you're using analytics in your application. You can also use other analytics module providers that are production-ready).
- [S3 File Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/file/s3/index.html.md) (or other file module providers that are production-ready).
- [SendGrid Notification Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/notification/sendgrid/index.html.md) (or other notification module providers that are production-ready).

The Caching Module was introduced in [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0) to replace the deprecated Cache Module.

Then, add these modules in `medusa-config.ts`:

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/caching",
      options: {
        providers: [
          {
            resolve: "@medusajs/caching-redis",
            id: "caching-redis",
            is_default: true,
            options: {
              redisUrl: process.env.CACHE_REDIS_URL,
            },
          },
        ],
      },
    },
    {
      resolve: "@medusajs/medusa/event-bus-redis",
      options: {
        redisUrl: process.env.REDIS_URL,
      },
    },
    {
      resolve: "@medusajs/medusa/workflow-engine-redis",
      options: {
        redis: {
          // Note: This was `url` before v2.12.2
          // It's now deprecated in favor of `redisUrl`
          redisUrl: process.env.REDIS_URL,
        },
      },
    },
    {
      resolve: "@medusajs/medusa/locking",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/locking-redis",
            id: "locking-redis",
            is_default: true,
            options: {
              redisUrl: process.env.LOCKING_REDIS_URL,
            },
          },
        ],
      },
    },

  ],
})
```

Check out the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) and [Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) documentation for other modules and providers to use.

***

## 4. Create PostgreSQL and Redis Databases

Your Medusa application must connect to PostgreSQL and Redis databases. So, before you deploy it, create production PostgreSQL and Redis databases.

If your hosting provider doesn't support databases, you can use [Neon for PostgreSQL database hosting](https://neon.tech/), and [Redis Cloud for the Redis database hosting](https://redis.io/cloud/).

After hosting both databases, keep their connection URLs for the next steps.

***

## 5. Deploy Medusa Application in Server Mode

As mentioned earlier, you'll deploy two instances or create two deployments of your Medusa application: one in server mode, and the other in worker mode.

The deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment.

### Set Environment Variables

When setting the environment variables of the Medusa application, set the following variables:

```bash
COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET
JWT_SECRET=supersecret  # TODO GENERATE SECURE SECRET
STORE_CORS= # STOREFRONT URL
ADMIN_CORS= # ADMIN URL
AUTH_CORS= # STOREFRONT AND ADMIN URLS, SEPARATED BY COMMAS
DISABLE_MEDUSA_ADMIN=false
MEDUSA_WORKER_MODE=server
PORT=9000
DATABASE_URL= # POSTGRES DATABASE URL
REDIS_URL= # REDIS DATABASE URL
```

Where:

- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret.
- `STORE_CORS`'s value is the URL of your storefront. If you don’t have it yet, you can skip adding it for now.
- `ADMIN_CORS`'s value is the URL of the admin dashboard, which is the same as the server Medusa application. You can add it later if you don't currently have it.
- `AUTH_CORS`'s value is the URLs of any application authenticating users, customers, or other actor types, such as the storefront and admin URLs. The URLs are separated by commas. If you don’t have the URLs yet, you can set its value later.
- Set `DISABLE_MEDUSA_ADMIN`'s value to `false` so that the admin is built with the server application.
- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL`
- Set the Redis database's connection URL as the value of `REDIS_URL`.

Feel free to add any other relevant environment variables, such as for integrations and Infrastructure Modules. If you're using environment variables in your admin customizations, make sure to set them as well, as they're inlined during the build process.

### Set Start Command

The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it.

If your hosting provider doesn't support setting a current-working directory, set the start command to the following:

```bash npm2yarn
cd .medusa/server && npm install && npm run predeploy && npm run start
```

Notice that you run the `predeploy` command before starting the Medusa application to run migrations and sync links whenever there's an update.

### Set Backend URL in Admin Configuration

The Medusa Admin is built and hosted statically. To send requests to the Medusa server application, you must set the backend URL in the Medusa Admin's configuration.

After you’ve obtained the Medusa application’s URL, add the following configuration to `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  admin: {
    // ...
    backendUrl: process.env.MEDUSA_BACKEND_URL,
  },
})
```

Then, push the changes to the GitHub repository or deployed application.

In your hosting provider, add or modify the following environment variables for the Medusa application in server mode:

```bash
ADMIN_CORS= # MEDUSA APPLICATION URL
AUTH_CORS= # ADD MEDUSA APPLICATION URL
MEDUSA_BACKEND_URL= # URL TO DEPLOYED MEDUSA APPLICATION
```

Where you set the value of `ADMIN_CORS` and `MEDUSA_BACKEND_URL` to the Medusa application’s URL, and you add the URL to `AUTH_CORS`.

After setting the environment variables, make sure to restart the deployment for the changes to take effect.

Remember to separate URLs in `AUTH_CORS` by commas.

***

## 6. Deploy Medusa Application in Worker Mode

Next, you'll deploy the Medusa application in worker mode.

As explained in the previous section, the deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment.

### Set Environment Variables

When setting the environment variables of the Medusa application, set the following variables:

```bash
COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET
JWT_SECRET=supersecret  # TODO GENERATE SECURE SECRET
DISABLE_MEDUSA_ADMIN=true
MEDUSA_WORKER_MODE=worker
PORT=9000
DATABASE_URL= # POSTGRES DATABASE URL
REDIS_URL= # REDIS DATABASE URL
```

Where:

- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret.
- Set `DISABLE_MEDUSA_ADMIN`'s value to `true` so that the admin isn't built with the worker application.
- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL`
- Set the Redis database's connection URL as the value of `REDIS_URL`.

Feel free to add any other relevant environment variables, such as for integrations and Infrastructure Modules.

### Set Start Command

The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it.

If your hosting provider doesn't support setting a current-working directory, set the start command to the following:

```bash npm2yarn
cd .medusa/server && npm install && npm run start
```

***

## 7. Test Deployed Application

Once the application is deployed and live, go to `<APP_URL>/health`, where `<APP_URL>` is the URL of the Medusa application in server mode. If the deployment was successful, you’ll see the `OK` response.

The Medusa Admin is also available at `<APP_URL>/app`.

***

## Create Admin User

If your hosting provider supports running commands in your Medusa application's directory, run the following command to create an admin user:

```bash
npx medusa user -e admin-medusa@test.com -p supersecret
```

Replace the email `admin-medusa@test.com` and password `supersecret` with the credentials you want.

You can use these credentials to log into the Medusa Admin dashboard.


# Medusa Deployment Overview

In this chapter, you’ll learn the general approach to deploying the Medusa application.

Want Medusa to manage and maintain your infrastructure? [Sign up and learn more about Cloud](https://docs.medusajs.com/cloud/index.html.md)

Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Cloud hosts your server, Admin dashboard, storefront, database, and Redis instance.

With Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub:

- Push to deploy.
- Multiple testing environments.
- Preview environments for new PRs.
- Test on production-like data.
- Storefront deployment.
- Emails setup.
- [And more](https://docs.medusajs.com/cloud#cloud-features/index.html.md).

## Medusa Project Components

A standard Medusa project is made up of:

- Medusa application: The Medusa server and the Medusa Admin.
- One or more storefronts

![Medusa deployment architecture showing the relationship between three main components: the Medusa server (backend API), Medusa Admin dashboard (for store management), and customer-facing storefronts, all connected to facilitate complete ecommerce functionality](https://res.cloudinary.com/dza7lstvk/image/upload/v1708600807/Medusa%20Book/deployment-options_ceuuvo.jpg)

You deploy the Medusa application, with the server and admin, separately from the storefront.

***

## Deploying the Medusa Application

You must deploy the Medusa application before the storefront, as it connects to the server and won’t work without a deployed Medusa server URL.

The Medusa application must be deployed to a hosting provider supporting Node.js server deployments.

![Medusa server deployment infrastructure diagram illustrating the backend services ecosystem: the Node.js Medusa server connected to essential services including PostgreSQL database for data storage, and Redis for caching and session management](https://res.cloudinary.com/dza7lstvk/image/upload/v1708600972/Medusa%20Book/backend_deployment_pgexo3.jpg)

Your server connects to a PostgreSQL database, Redis, and other services relevant for your setup. Most hosting providers support deploying and managing these databases along with your Medusa server.

When you deploy your Medusa application, you also deploy the Medusa Admin. For optimal experience, your hosting provider and plan must offer at least 2GB of RAM.

### Deploy Server and Worker Instances

By default, Medusa runs all processes in a single instance. This includes the server that handles incoming requests and the worker that processes background tasks. While this works for development, it’s not optimal for production environments as many background tasks can be long-running or resource-heavy.

Instead, you should deploy two instances:

- A server instance, which handles incoming requests to the application’s API routes.
- A worker instance, which processes background tasks, including scheduled jobs and subscribers.

You don’t need to set up different projects for each instance. Instead, you can configure the Medusa application to run in different modes based on environment variables.

Learn more about worker modes and how to configure them in the [Worker Mode chapter](https://docs.medusajs.com/learn/production/worker-mode/index.html.md).

### How to Deploy Medusa?

Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Cloud hosts your server, Admin dashboard, database, and Redis instance.

With Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub:

- Push to deploy.
- Multiple testing environments.
- Preview environments for new PRs.
- Test on production-like data.

[Sign up and learn more about Cloud](https://docs.medusajs.com/cloud/index.html.md)

To self-host Medusa, the [next chapter](https://docs.medusajs.com/learn/deployment/general/index.html.md) explains the general steps to deploy your Medusa application. Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for general and specific hosting providers.

***

## Deploying the Storefront

The storefront is deployed separately from the Medusa application, and the hosting options depend on the tools and frameworks you use to create the storefront.

If you’re using the Next.js Starter storefront, you may deploy the storefront to any hosting provider that supports frontend frameworks, such as Vercel.

Per Vercel’s [license and plans](https://vercel.com/pricing), their free plan can only be used for personal, non-commercial projects. So, you can deploy the storefront on the free plan for development purposes, but for commercial projects, you must update your Vercel plan.

Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for specific hosting providers.


# Admin Development Constraints

This chapter lists some development constraints of admin widgets and UI routes.

## Arrow Functions

Widget and UI route components must be created as arrow functions. Otherwise, Medusa doesn't register them correctly.

```ts highlights={arrowHighlights}
// Don't
function ProductWidget() {
  // ...
}

// Do
const ProductWidget = () => {
  // ...
}
```

***

## Widget Zone

A widget zone's value must be wrapped in double or single quotes. It can't be a template literal or a variable. Otherwise, Medusa doesn't register the widget correctly.

```ts highlights={zoneHighlights}
// Don't
export const config = defineWidgetConfig({
  zone: `product.details.before`,
})

// Don't
const ZONE = "product.details.after"
export const config = defineWidgetConfig({
  zone: ZONE,
})

// Do
export const config = defineWidgetConfig({
  zone: "product.details.before",
})
```


# Environment Variables in Admin Customizations

In this chapter, you'll learn how to use environment variables in your admin customizations.

To learn how environment variables are generally loaded in Medusa based on your application's environment, check out [this chapter](https://docs.medusajs.com/learn/fundamentals/environment-variables/index.html.md).

## How to Set Environment Variables

This only applies to customizations in a Medusa project. For plugins, refer to the [Environment Variables in Plugins](#environment-variables-in-plugins) section.

The Medusa Admin is built on top of [Vite](https://vite.dev/). To set an environment variable that you want to use in a widget or UI route, prefix the environment variable with `VITE_`.

For example:

```plain
VITE_MY_API_KEY=sk_123
```

***

## How to Use Environment Variables

To access or use an environment variable starting with `VITE_`, use the `import.meta.env` object.

For example:

```tsx highlights={[["8"]]}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

const ProductWidget = () => {
  return (
    <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.VITE_MY_API_KEY}</Heading>
      </div>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

In this example, you display the API key in a widget using `import.meta.env.VITE_MY_API_KEY`.

### Type Error on import.meta.env

If you receive a type error on `import.meta.env`, create the file `src/admin/vite-env.d.ts` with the following content:

```ts title="src/admin/vite-env.d.ts"
/// <reference types="vite/client" />

declare const __BASE__: string
declare const __BACKEND_URL__: string
declare const __STOREFRONT_URL__: string
```

This file tells TypeScript to recognize the `import.meta.env` object and enhances the types of your custom environment variables.

Note that the `__BASE__`, `__BACKEND_URL__`, and `__STOREFRONT_URL__` variables are global variables available in your admin customizations. Learn more in the [Tips for Admin Customizations](https://docs.medusajs.com/learn/fundamentals/admin/tips#global-variables-in-admin-customizations/index.html.md) chapter.

***

## Check Node Environment in Admin Customizations

To check the current environment, Vite exposes two variables:

- `import.meta.env.DEV`: Returns `true` if the current environment is development.
- `import.meta.env.PROD`: Returns `true` if the current environment is production.

Learn more about other Vite environment variables in the [Vite documentation](https://vite.dev/guide/env-and-mode).

***

## Predefined Environment Variables

You can further customize the Medusa Admin behavior using the following predefined environment variables. You can set the environment variables in your `.env` file or in your deployment platform.

The following pre-defined environment variables are available starting [Medusa v2.12.0](https://github.com/medusajs/medusa/releases/tag/v2.12.0).

|Environment Variable|Description|Default|
|---|---|---|
|\`ADMIN\_AUTH\_TYPE\`|A string indicating the authentication method that the JS SDK instance uses in the Medusa Admin. Possible values are |\`session\`|
|\`ADMIN\_JWT\_TOKEN\_STORAGE\_KEY\`|A string indicating the key used to store the authentication JWT token in the browser's local storage. Only applicable if |\`medusa\_auth\_token\`|

***

## Environment Variables in Production

When you build the Medusa application, including the Medusa Admin, with the `build` command, the environment variables are inlined into the build. This means that you can't change the environment variables without rebuilding the application.

For example, the `VITE_MY_API_KEY` environment variable in the example above will be replaced with the actual value during the build process.

***

## Environment Variables 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.

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`).
- `__STOREFRONT_URL__`: The URL of the Medusa Storefront, as set in the [Medusa configurations](https://docs.medusajs.com/learn/configurations/medusa-config#storefronturl/index.html.md).

You can use those variables in your Medusa Admin customizations of a plugin. For example:

```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">Backend URL: {__BACKEND_URL__}</Heading>
      </div>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

To fix possible type errors, create the file `src/admin/vite-env.d.ts` and add the global variables:

```ts title="src/admin/vite-env.d.ts"
/// <reference types="vite/client" />

declare const __BACKEND_URL__: string
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

In this chapter, you'll learn about the Medusa Admin dashboard and the possible ways to customize it.

To explore the Medusa Admin's commerce features, check out the [User Guide](https://docs.medusajs.com/user-guide/index.html.md). These user guides are designed for merchants and provide the steps to perform any task within the Medusa Admin.

## What is the Medusa Admin?

The Medusa Admin is an intuitive dashboard that allows merchants to manage their ecommerce store. It provides management features related to products, orders, customers, and more.

The Medusa Admin is built with [Vite v5](https://v5.vite.dev/). When you [install the Medusa application](https://docs.medusajs.com/learn/installation/index.html.md), you also install the Medusa Admin. Then, when you start the Medusa application, you can access the Medusa Admin at `http://localhost:9000/app`.

If you don't have an admin user, use the [Medusa CLI](https://docs.medusajs.com/resources/medusa-cli/commands/user/index.html.md) to create one.

***

## How to Customize the Medusa Admin?

You can customize the Medusa Admin dashboard by:

- Adding new sections to existing pages using [Widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md).
- Adding new pages using [UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md).

The next chapters will cover these two topics in detail.

### What You Can't Customize in the Medusa Admin

You can't customize the admin dashboard's layout, design, or the content of the existing pages (aside from injecting widgets). You also can't change the Medusa logo used in the admin dashboard.

If your use case requires heavy customization of the admin dashboard, you can build a custom admin dashboard using Medusa's [Admin API routes](https://docs.medusajs.com/api/admin).

***

## Medusa UI Package

Medusa provides a Medusa UI package to facilitate your admin development through ready-made components and ensure a consistent design between your customizations and the dashboard’s design.

Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/index.html.md) to learn how to install it and use its components.

***

## Admin How-to Guides

The [Admin How-to Guides](https://docs.medusajs.com/resources/how-to-tutorials#admin/index.html.md) provides guides that walk you through common admin customizations, such as [building common admin components](https://docs.medusajs.com/resources/admin-components/index.html.md), or [adding third-party authentication to the admin](https://docs.medusajs.com/resources/how-to-tutorials/how-to/admin/auth/index.html.md).

Refer to these guides for practical examples of customizing the Medusa Admin dashboard.


# Admin Routing Customizations

The Medusa Admin dashboard uses [React Router](https://reactrouter.com) under the hood to manage routing. This gives you more flexibility in routing-related customizations using React Router's utilities, hooks, and components.

In this chapter, you'll learn about routing-related customizations that you can use in your widgets, UI routes, and settings pages using React Router.

`react-router-dom` is available in your project by default through the Medusa packages. You don't need to install it separately.

## Link to a Page

To link to a page in your admin customizations, you can use the `Link` component from `react-router-dom`. For example:

```tsx title="src/admin/widgets/product-widget.tsx" highlights={highlights}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container } from "@medusajs/ui"
import { Link } from "react-router-dom"

// The widget
const ProductWidget = () => {
  return (
    <Container className="divide-y p-0">
      <Link to={"/orders"}>View Orders</Link>
    </Container>
  )
}

// The widget's configurations
export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

This adds a widget to a product's details page with a link to the Orders page. The link's path must be without the `/app` prefix.

***

## Fetch Data with Route Loaders

Route loaders are available starting from Medusa v2.5.1.

In your UI routes and settings pages, you may need to retrieve data to use in your route component. For example, you may want to fetch a list of products to display on a custom page.

The recommended approach is to fetch data within the UI route component asynchronously using the JS SDK with Tanstack (React) Query as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md).

However, if you need the data to be fetched before the route is rendered, such as if you're [setting breadcrumbs dynamically](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes#set-breadcrumbs-dynamically/index.html.md), you can use a route loader.

To fetch data with a route loader:

1. Define and export a [React Router loader](https://reactrouter.com/6.29.0/route/loader#loader) function in the UI route's file. In this function, you can fetch and return data asynchronously.
2. In your UI route's component, you can use the [useLoaderData hook from React Router](https://reactrouter.com/6.29.0/hooks/use-loader-data#useloaderdata) to access the data returned by the `loader` function.

For example, consider the following UI route created at `src/admin/routes/custom/page.tsx`:

```tsx title="src/admin/routes/custom/page.tsx" highlights={loaderHighlights}
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"
import {
  useLoaderData,
} from "react-router-dom"

export async function loader() {
  // TODO fetch products

  return {
    products: [],
  }
}

const CustomPage = () => {
  const { products } = useLoaderData() as Awaited<ReturnType<typeof loader>>

  return (
    <div>
      <Container className="divide-y p-0">
        <div className="flex items-center justify-between px-6 py-4">
          <Heading level="h2">Products count: {products.length}</Heading>
        </div>
      </Container>
    </div>
  )
}

export default CustomPage
```

In this example, you first export a `loader` function that can be used to fetch data, such as products. The function returns an object with a `products` property.

Then, in the `CustomPage` route component, you use the `useLoaderData` hook from React Router to access the data returned by the `loader` function. You can then use the data in your component.

### Route Loaders Block Rendering

Route loaders block the rendering of your UI route until the data is fetched, which may negatively impact the user experience. So, only use route loaders when the route component needs essential data before rendering, or if you're preparing data that doesn't require sending API requests.

Otherwise, use the JS SDK with Tanstack (React) Query in the UI route component as explained in the [Tips](https://docs.medusajs.com/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md) chapter to fetch data asynchronously and update the UI when the data is available.

![Timeline comparison of a UI route with and without a route loader](https://res.cloudinary.com/dza7lstvk/image/upload/v1753428567/Medusa%20Book/ui-route-loading_vycev8.jpg)

### Access Route Parameters in Loader

You can access route parameters in the loader function. For example, consider the following UI route created at `src/admin/routes/custom/[id]/page.tsx`:

```tsx title="src/admin/routes/custom/[id]/page.tsx" highlights={loaderParamHighlights}
import { Container, Heading } from "@medusajs/ui"
import {
  useLoaderData,
  LoaderFunctionArgs,
} from "react-router-dom"

export async function loader({ params }: LoaderFunctionArgs) {
  const { id } = params
  // TODO fetch product by id

  return {
    id,
  }
}

const CustomPage = () => {
  const { id } = useLoaderData() as Awaited<ReturnType<typeof loader>>

  return (
    <div>
      <Container className="divide-y p-0">
        <div className="flex items-center justify-between px-6 py-4">
          <Heading level="h2">Product ID: {id}</Heading>
        </div>
      </Container>
    </div>
  )
}

export default CustomPage
```

Because the UI route has a route parameter `[id]`, you can access the `id` parameter in the `loader` function. The loader function accepts as a parameter an object that has a `params` property containing the route parameters.

In the loader, you can fetch data asynchronously using the route parameter and return it. Then, in the route component, you can access the data using the `useLoaderData` hook.

***

## Other React Router Utilities

### Route Handles

Route handles are available starting from Medusa v2.5.1.

In your UI route or any route file, you can export a `handle` object to define [route handles](https://reactrouter.com/start/framework/route-module#handle). The object is passed to the loader and route contexts.

For example:

```tsx title="src/admin/routes/custom/page.tsx"
export const handle = {
  sandbox: true,
}
```

You can also use the `handle` object to define a breadcrumb for the route. Learn more in the [UI Route](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md) chapter.

### React Router Components and Hooks

Refer to [react-router-dom’s documentation](https://reactrouter.com/en/6.29.0) for components and hooks that you can use in your admin customizations.


# Admin Development Tips

In this chapter, you'll find some tips for developing your admin.

## Send Requests to API Routes

To send a request to an API route in the Medusa Application, use Medusa's [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) with [Tanstack Query](https://tanstack.com/query/latest). Both of these tools are installed in your project by default.

Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency.

First, create the file `src/admin/lib/sdk.ts` to set up the SDK for use in your customizations:

### Medusa Project

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

### Medusa Plugin

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  // add __BACKEND_URL__ to src/admin/vite-env.d.ts if you get type errors
  baseUrl: __BACKEND_URL__ || "/",
  auth: {
    type: "session",
  },
})
```

Notice that in a Medusa project, you use `import.meta.env` to access environment variables in your customizations, whereas in a plugin you use the global variable `__BACKEND_URL__` to access the backend URL. You can learn more in the [Admin Environment Variables](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md) chapter.

Refer to the [JS SDK](https://docs.medusajs.com/resources/js-sdk#js-sdk-configurations/index.html.md) reference for more details on the SDK's configurations.

Then, use the configured SDK with the `useQuery` Tanstack Query hook to send `GET` requests, and the `useMutation` hook to send `POST` or `DELETE` requests.

For example:

### Query

```tsx title="src/admin/widgets/product-widget.tsx" highlights={queryHighlights}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container } from "@medusajs/ui"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"

const ProductWidget = () => {
  const { data, isLoading } = useQuery({
    queryFn: () => sdk.admin.product.list(),
    queryKey: ["products"],
  })
  
  return (
    <Container className="divide-y p-0">
      {isLoading && <span>Loading...</span>}
      {data?.products && (
        <ul>
          {data.products.map((product) => (
            <li key={product.id}>{product.title}</li>
          ))}
        </ul>
      )}
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.list.before",
})

export default ProductWidget
```

### Mutation

```tsx title="src/admin/widgets/product-widget.tsx" highlights={mutationHighlights}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Button, Container } from "@medusajs/ui"
import { useMutation } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"
import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types"

const ProductWidget = ({ 
  data: productData,
}: DetailWidgetProps<HttpTypes.AdminProduct>) => {
  const { mutateAsync } = useMutation({
    mutationFn: (payload: HttpTypes.AdminUpdateProduct) => 
      sdk.admin.product.update(productData.id, payload),
    onSuccess: () => alert("Product updated"),
  })

  const handleUpdate = () => {
    mutateAsync({
      title: "New Product Title",
    })
  }
  
  return (
    <Container className="divide-y p-0">
      <Button onClick={handleUpdate}>Update Title</Button>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

You can also send requests to custom routes, as explained in the [JS SDK reference](https://docs.medusajs.com/resources/js-sdk/index.html.md).

### Use Route Loaders for Initial Data

You may need to retrieve data before your component is rendered, or you may need to pass some initial data to your component to be used while data is being fetched. In those cases, you can use a [route loader](https://docs.medusajs.com/learn/fundamentals/admin/routing/index.html.md).

### Troubleshooting

#### No QueryClient set

If you're using `pnpm` as a package manager in your Medusa application, you might face the following error when you try to access the admin dashboard:

```bash
No QueryClient set, use QueryClientProvider to set one
```

This is a known issue when using `pnpm`. To fix it, you need to install the `@tanstack/react-query` package manually. Make sure to install the same version installed in [@medusajs/dashboard](https://github.com/medusajs/medusa/blob/d58462c9c91a3e335d03f758438e109caad1f839/packages/admin/dashboard/package.json#L54).

Learn more in the [Errors with pnpm](https://docs.medusajs.com/resources/troubleshooting/pnpm/index.html.md) troubleshooting guide.

***

## Global Variables in Admin Customizations

In your admin customizations, you can use the following global variables:

- `__BASE__`: The base path of the Medusa Admin, as set in the [admin.path](https://docs.medusajs.com/learn/configurations/medusa-config#path/index.html.md) configuration in `medusa-config.ts`.
- `__BACKEND_URL__`: The URL to the Medusa backend, as set in the [admin.backendUrl](https://docs.medusajs.com/learn/configurations/medusa-config#backendurl/index.html.md) configuration in `medusa-config.ts`.
- `__STOREFRONT_URL__`: The URL to the storefront, as set in the [admin.storefrontUrl](https://docs.medusajs.com/learn/configurations/medusa-config#storefrontUrl/index.html.md) configuration in `medusa-config.ts`.

If you get type errors while using these variables, create the file `src/admin/vite-env.d.ts` with the following content:

```ts title="src/admin/vite-env.d.ts"
/// <reference types="vite/client" />

declare const __BASE__: string
declare const __BACKEND_URL__: string
declare const __STOREFRONT_URL__: string
```

***

## Admin Translations

The Medusa Admin dashboard can be displayed in languages other than English, which is the default. Other languages are added through community contributions.

Learn how to add a new language translation for the Medusa Admin in [this guide](https://docs.medusajs.com/learn/resources/contribution-guidelines/admin-translations/index.html.md).


# Translate Admin Customizations

In this chapter, you'll learn how to add translations to your Medusa Admin widgets and UI routes.

Translations for admin customizations are available from [Medusa v2.11.1](https://github.com/medusajs/medusa/releases/tag/v2.11.1).

## Translations in the Medusa Admin

The Medusa Admin dashboard supports [multiple languages](https://docs.medusajs.com/user-guide/tips/languages/index.html.md) for its interface. Medusa uses [react-i18next](https://react.i18next.com/) to manage translations in the admin dashboard.

Medusa Admin translations apply to the interface only. To translate content like product data, check out the [Translations user guide](https://docs.medusajs.com/user-guide/settings/translations/index.html.md).

When you create [Widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md) or [UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md) to customize the Medusa Admin, you can provide translations for the text content in your customizations, allowing users to view your customizations in their preferred language.

You can add translations for your admin customizations within your Medusa project or as part of a plugin.

***

## How to Add Translations to Admin Customizations

### Step 1: Create Translation Files

Translation files are JSON files containing key-value pairs, where the key identifies a text string and the value is the translated text or a nested object of translations.

For example, to add English translations, create the file `src/admin/i18n/json/en.json` with the following content:

English is the default language for Medusa Admin, so it's recommended to always include an English translation file.

![Directory structure showing where to place translation files](https://res.cloudinary.com/dza7lstvk/image/upload/v1761555573/Medusa%20Book/translations-json_m2pvet.jpg)

```json title="src/admin/i18n/json/en.json"
{
  "brands": {
    "title": "Brands",
    "description": "Manage your product brands"
  },
  "done": "Done"
}
```

You can create additional translation files for other languages by following the same structure. For example, for Spanish, create `src/admin/i18n/json/es.json`:

![Directory structure showing where to place translation files](https://res.cloudinary.com/dza7lstvk/image/upload/v1761555573/Medusa%20Book/translations-json-es_s1etna.jpg)

```json title="src/admin/i18n/json/es.json"
{
  "brands": {
    "title": "Marcas",
    "description": "Gestiona las marcas de tus productos"
  },
  "done": "Hecho"
}
```

### Step 2: Load Translation Files

Next, to load the translation files, create the file `src/admin/i18n/index.ts` with the following content:

![Directory structure showing i18n index file](https://res.cloudinary.com/dza7lstvk/image/upload/v1761555573/Medusa%20Book/translations-index_cgrj0t.jpg)

```ts title="src/admin/i18n/index.ts"
import en from "./json/en.json" with { type: "json" }
import es from "./json/es.json" with { type: "json" }

export default {
  en: {
    translation: en,
  },
  es: {
    translation: es,
  },
}
```

The `src/admin/i18n/index.ts` file imports the JSON translation files. You must include the `with { type: "json" }` directive to ensure the JSON files load correctly.

The file exports an object that maps two-character language codes (like `en` and `es`) to their respective translation data.

### Step 3: Use Translations in Admin Customizations

Finally, you can use the translations in your admin customizations by using the `useTranslation` hook from `react-i18next`.

The `react-i18next` package is already included in the Medusa Admin's dependencies, so you don't need to install it separately. However, `pnpm` users may need to install it manually due to package resolution issues.

For example, create the file `src/admin/widgets/product-brand.tsx` with the following content:

```tsx title="src/admin/widgets/product-brand.tsx" highlights={translationHighlights}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Button, Container, Heading } from "@medusajs/ui"
import { useTranslation } from "react-i18next"

const ProductWidget = () => {
  const { t } = useTranslation()
  return (
    <Container className="p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">{t("brands.title")}</Heading>
        <p>{t("brands.description")}</p>
      </div>
      <div className="flex justify-end px-6 py-4">
        <Button variant="primary">{t("done")}</Button>
      </div>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

In the above example, you retrieve the `t` function from the `useTranslation` hook. You then use this function to get the translated text by providing the appropriate keys defined in your translation JSON files.

Nested keys are joined using dot notation. For example, `brands.title` refers to the `title` key inside the `brands` object in the translation files.

### Test Translations

To test the translations, start the Medusa application with the following command:

```bash npm2yarn
npm run dev
```

Then, go to a product details page in the Medusa Admin dashboard. If your default language is set to English, you'll see the widget displaying text in English.

Next, [change the admin language](https://docs.medusajs.com/user-guide/settings/profile#edit-profile-details/index.html.md) to Spanish. The widget will now display the text in Spanish.

***

## Translation Auto-Completion

Translation auto-completion is available from [Medusa v2.11.2](https://github.com/medusajs/medusa/releases/tag/v2.11.2).

To enhance your development experience, you can set up auto-completion for translation keys. This will allow you to auto-complete translation keys from Medusa's default translations as well as your custom translations.

To set up auto-completion, create the file `src/admin/i18next.d.ts` with the following content:

```ts title="src/admin/i18n/i18next.d.ts"
// Import Medusa keys
import type { Resources } from "@medusajs/dashboard"
// Import your custom translation keys
// For example, import the English translation file
import type enTranslation from "./i18n/en.json"
// add other imports for different languages if needed...
// import type esTranslation from "./i18n/es.json"

declare module "i18next" {
    interface CustomTypeOptions {
        fallbackNS: "translation"
        resources: {
            translation: Resources["translation"]                     
            // Optional: add custom namespaces here
            // For example, if you have a custom namespace called 'brands':
            brands: typeof enTranslation & Resources["translation"]
        }
    }
}
```

***

## How Translations are Loaded

When you load the translations with the `translation` key in `src/admin/i18n/index.ts`, your custom Medusa Admin translations are merged with the default Medusa Admin translations:

- Translation keys in your custom translations override the default Medusa Admin translations.
- The default Medusa Admin translations are used as a fallback when a key is not defined in your custom translations.

For example, consider the following widget and translation file:

### Widget

```tsx title="src/admin/widgets/product-brand.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Button, Container, Heading } from "@medusajs/ui"
import { useTranslation } from "react-i18next"

const ProductWidget = () => {
  const { t } = useTranslation()
  return (
    <Container className="p-0">
      <div className="flex items-center justify-between px-6 py-4">
        {/* Output: Custom Brands Title */}
        <Heading level="h2">{t("brands.title")}</Heading>
        {/* Output: brands.description */}
        <p>{t("brands.description")}</p>
      </div>
      <div className="flex justify-end px-6 py-4">
        {/* Output: Custom Save */}
        <Button variant="primary">{t("actions.save")}</Button>
        {/* Output: Delete */}
        <Button variant="primary">{t("actions.delete")}</Button>
      </div>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

### Translation File

```json title="src/admin/i18n/json/en.json"
{
  "brands": {
    "title": "Custom Brands Title"
  },
  "actions": {
    "save": "Custom Save"
  }
}
```

### Loaded Translations

```ts title="src/admin/i18n/index.ts"
import en from "./json/en.json" with { type: "json" }

export default {
  en: {
    translation: en,
  },
  // other languages...
}
```

The widget will render the following for each translation key:

- `brands.title`: Defined in your custom translation file, so it outputs `Custom Brands Title`.
- `brands.description`: Not defined in your custom translation file or the default Medusa Admin translations, so it outputs the key itself: `brands.description`.
- `actions.save`: Defined in your custom translation file, so it outputs `Custom Save`.
- `actions.delete`: Not defined in your custom translation file, so it falls back to the default Medusa Admin translation and outputs `Delete`.

### Custom Translation Namespaces

To avoid potential key conflicts between your custom translations and the default Medusa Admin translations, you can use custom namespaces. This is particularly useful when developing plugins that add admin customizations, as it prevents naming collisions with other plugins or the default Medusa Admin translations.

To add translations under a custom namespace, change the `[language].translation` key in the `src/admin/i18n/index.ts` file to your desired namespace:

```ts title="src/admin/i18n/index.ts" highlights={namespacesHighlights}
import en from "./json/en.json" with { type: "json" }
import es from "./json/es.json" with { type: "json" }

export default {
  en: {
    brands: en,
  },
  es: {
    brands: es,
  },
}
```

The translation files will now be loaded under the `brands` namespace.

Then, in your admin customizations, specify the namespace when using the `useTranslation` hook:

```tsx title="src/admin/widgets/product-brand.tsx" highlights={[["7"]]}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Button, Container, Heading } from "@medusajs/ui"
import { useTranslation } from "react-i18next"

// The widget
const ProductWidget = () => {
  const { t } = useTranslation("brands")
  return (
    <Container className="p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">{t("brands.title")}</Heading>
        <p>{t("brands.description")}</p>
      </div>
      <div className="flex justify-end px-6 py-4">
        <Button variant="primary">{t("done")}</Button>
      </div>
    </Container>
  )
}

// The widget's configurations
export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

Translations are now loaded only from the `brands` namespace without conflicting with other translation keys in the Medusa Admin.

***

## Translation Tips

### Translation Organization

To keep your translation files organized, especially as they grow, consider grouping related translation keys into nested objects. This helps maintain clarity and structure.

It's recommended to create a nested object for each domain (for example, `brands`, `products`, etc...) and place related translation keys within those objects. This makes it easier to manage and locate specific translations.

For example:

```json title="src/admin/i18n/json/en.json"
{
  "brands": {
    "title": "Brands",
    "description": "Manage your product brands",
    "actions": {
      "add": "Add Brand",
      "edit": "Edit Brand"
    }
  }
}
```

You can then access these nested translations using dot notation, such as `brands.title` or `brands.actions.add`.

### Variables in Translations

Translation values can include variables that are dynamically replaced at runtime. Variables are defined using double curly braces `{{variableName}}` in the translation files.

For example, in your translation file `src/admin/i18n/json/en.json`, define a translation with a variable:

```json title="src/admin/i18n/json/en.json"
{
  "welcome_message": "Welcome, {{username}}!"
}
```

Then, in your admin customization, pass the variable value in the second object parameter of the `t` function:

```tsx title="src/admin/widgets/welcome-widget.tsx"
t("welcome_message", { username: "John" })
```

This will output: `Welcome, John!`

### Pluralization

The `t` function supports pluralization based on a count value. You can define singular and plural forms in your translation files using the `_one`, `_other`, and `_zero` suffixes.

For example, in your translation file `src/admin/i18n/json/en.json`, define the following translations:

```json title="src/admin/i18n/json/en.json"
{
  "item_count_one": "You have {{count}} item.",
  "item_count_other": "You have {{count}} items.",
  "item_count_zero": "You have no items."
}
```

Then, in your admin customization, use the key without the suffix and provide the `count` variable:

```tsx title="src/admin/widgets/item-count-widget.tsx"
t("item_count", { count: itemCount })
```

This will render one of the following based on the value of `itemCount`:

1. If `itemCount` is `0`, `item_count_zero` is used: `You have no items.`
2. If `itemCount` is `1`, `item_count_one` is used: `You have 1 item.`
3. If `itemCount` is greater than `1`, `item_count_other` is used: `You have X items.`

### Element Interpolation

Your translation strings can include HTML or React element placeholders that are replaced with actual elements at runtime. This is useful for adding links, bold text, or other formatting within translated strings.

Elements to be interpolated are defined using angle brackets `<index></index>` in the translation files, where `index` is a zero-based index representing the element's position.

For example, in your translation file `src/admin/i18n/json/en.json`, define a translation with element placeholders:

```json title="src/admin/i18n/json/en.json"
{
  "terms_and_conditions": "Please read our <0>Terms and Conditions</0>."
}
```

Then, in your admin customization, import the `Trans` component from `react-i18next` that allows you to interpolate elements:

```tsx title="src/admin/widgets/terms-widget.tsx"
import { Trans } from "react-i18next"
```

Finally, use the `Trans` component in the `return` statement to render the translation with the interpolated elements:

```tsx title="src/admin/widgets/terms-widget.tsx"
<Trans
  i18nKey="terms_and_conditions"
  components={[
    <a href="https://example.com/terms" className="text-blue-600 underline" />,
  ]}
/>
```

The `components` prop is an array of React elements that correspond to the placeholders defined in the translation string. In this case, the `<0></0>` placeholder is replaced with the anchor `<a>` element.

#### Passing Variables with Element Interpolation

You can also pass translation variables to the `Trans` component as props. For example, to include a username variable:

```tsx title="src/admin/widgets/welcome-widget.tsx"
<Trans
  i18nKey="welcome_message"
  username="John"
  components={[
    <strong />,
  ]}
/>
```

The `username` prop replaces the `{{username}}` variable in the translation string, and the `<0></0>` placeholder is replaced with the `<strong>` element.

#### Using Namespaces with Element Interpolation

If you're loading translations from a custom namespace, specify the namespace in the `Trans` component using the `ns` prop:

```tsx title="src/admin/widgets/product-brand.tsx"
<Trans
  i18nKey="brands.count"
  ns="brands"
  count={5}
  components={[<strong />]}
/>
```

The `ns` prop indicates that the translation should be loaded from the `brands` namespace.

#### Multiple Element Interpolation

You can interpolate multiple elements by defining multiple placeholders in the translation string and providing corresponding elements in the `components` array.

For example, define the following translation string:

```json title="src/admin/i18n/json/en.json"
{
  "welcome_message": "Hello, <0>{{username}}</0>! Please read our <1>Terms and Conditions</1>."
}
```

Then, in your admin customization, you can use the `Trans` component with multiple elements:

```tsx title="src/admin/widgets/welcome-widget.tsx"
<Trans
  i18nKey="welcome_message"
  username="John"
  components={[
    <strong />,
    <a href="https://example.com/terms" className="text-blue-600 underline" />,
  ]}
/>
```

The first placeholder `<0></0>` is replaced with the `<strong>` element, and the second placeholder `<1></1>` is replaced with the anchor `<a>` element.


# Admin UI Routes

In this chapter, you’ll learn how to create a UI route in the admin dashboard.

## What is a UI Route?

The Medusa Admin dashboard is customizable, allowing you to add new pages, called UI routes. You create a UI route as a React component showing custom content that allows admin users to perform custom actions.

For example, you can add a new page to show and manage product reviews, which aren't available natively in Medusa.

You can create a UI route directly in your Medusa application, or in a [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) if you want to share the UI route across multiple Medusa applications.

***

## How to Create a UI Route?

### Prerequisites

- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md)

You create a UI route in a `page.tsx` file under a sub-directory of `src/admin/routes` directory. The file's path relative to `src/admin/routes` determines its path in the dashboard. The file’s default export must be the UI route’s React component.

For example, create the file `src/admin/routes/custom/page.tsx` with the following content:

![Example of UI route file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867243/Medusa%20Book/ui-route-dir-overview_tgju25.jpg)

```tsx title="src/admin/routes/custom/page.tsx"
import { Container, Heading } from "@medusajs/ui"

const CustomPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">This is my custom route</Heading>
      </div>
    </Container>
  )
}

export default CustomPage
```

You add a new route at `http://localhost:9000/app/custom`. The `CustomPage` component holds the page's content, which currently only shows a heading.

In the route, you use [Medusa UI](https://docs.medusajs.com/ui/index.html.md), a package that Medusa maintains to allow you to customize the dashboard with the same components used to build it.

The UI route component must be created as an arrow function.

### Test the UI Route

To test the UI route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, after logging into the admin dashboard, open the page `http://localhost:9000/app/custom` to see your custom page.

***

## Show UI Route in the Sidebar

To add a sidebar item for your custom UI route, export a configuration object in the UI route's file:

```tsx title="src/admin/routes/custom/page.tsx" highlights={highlights}
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { Container, Heading } from "@medusajs/ui"

const CustomPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">This is my custom route</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Custom Route",
  icon: ChatBubbleLeftRight,
})

export default CustomPage
```

The configuration object is created using `defineRouteConfig` from the Medusa Framework. It accepts the following properties:

- `label`: the sidebar item's label.
- `icon`: an optional React component used as an icon in the sidebar.
- `rank`: an optional number to order the route among sibling routes. Learn more in the [Specify UI Route Sidebar Rank](#specify-ui-route-sidebar-rank) section.

The above example adds a new sidebar item with the label `Custom Route` and an icon from the [Medusa UI Icons package](https://docs.medusajs.com/ui/icons/overview/index.html.md).

### Specify UI Route Sidebar Rank

UI route ranking is available starting [Medusa v2.11.4](https://github.com/medusajs/medusa/releases/tag/v2.11.4).

By default, custom UI routes are added to the sidebar in the order their files are loaded. This applies to your custom UI routes, and UI routes defined in plugins.

You can specify the ranking of your UI route in the sidebar using the `rank` property passed to `defineRouteConfig`.

For example, consider you have the following UI routes:

### UI Route 1

```tsx title="src/admin/routes/analytics/page.tsx" highlights={[["18"]]}
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChartBar } from "@medusajs/icons"
import { Container, Heading } from "@medusajs/ui"

const AnalyticsPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">Analytics Dashboard</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Analytics",
  icon: ChartBar,
  rank: 1,
})

export default AnalyticsPage
```

### UI Route 2

```tsx title="src/admin/routes/reports/page.tsx" highlights={[["18"]]}
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { DocumentText } from "@medusajs/icons"
import { Container, Heading } from "@medusajs/ui"

const ReportsPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">Reports</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Reports",
  icon: DocumentText,
  rank: 2,
})

export default ReportsPage
```

In the sidebar, "Analytics" with the rank `1` will be added before "Reports" with the rank `2`.

#### How are UI Routes Sorted in the Sidebar

Medusa sorts custom UI routes based on their rank:

1. UI routes that have ranks are sorted in ascending order.
2. UI routes without a rank are added after the ranked UI routes.

Medusa also applies the same sorting logic to UI routes at the nested level. Learn more in the [Nested UI Routes Ranking](#nested-ui-routes-ranking) section.

### Nested UI Routes

Consider that alongside the UI route above at `src/admin/routes/custom/page.tsx` you create a nested UI route at `src/admin/routes/custom/nested/page.tsx` that also exports route configurations:

![Example of nested UI route file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867243/Medusa%20Book/ui-route-dir-overview_tgju25.jpg)

```tsx title="src/admin/routes/custom/nested/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

const NestedCustomPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">This is my nested custom route</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Nested Route",
})

export default NestedCustomPage
```

This UI route is shown in the sidebar as an item nested in the parent "Custom Route" item. Nested items are only shown when the parent sidebar items (in this case, "Custom Route") are clicked.

#### Caveats

Some caveats for nested UI routes in the sidebar:

- Nested dynamic UI routes, such as one created at `src/admin/routes/custom/[id]/page.tsx` aren't added to the sidebar as it's not possible to link to a dynamic route. If the dynamic route exports route configurations, a warning is logged in the browser's console.
- Nested routes in settings pages aren't shown in the sidebar to follow the admin's design conventions.
- The `icon` configuration is ignored for the sidebar item of nested UI routes to follow the admin's design conventions.

### Route Under Existing Admin Route

You can add a custom UI route under an existing route. For example, you can add a route under the orders route:

```tsx title="src/admin/routes/orders/nested/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

const NestedOrdersPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h1">Nested Orders Page</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Nested Orders",
  nested: "/orders",
})

export default NestedOrdersPage
```

The `nested` property passed to `defineRouteConfig` specifies which route this custom route is nested under. This route will now show in the sidebar under the existing "Orders" sidebar item.

#### Nested UI Routes Ranking

Nested UI routes also accept the [rank](#specify-ui-route-sidebar-rank) configuration. It allows you to specify the order that the nested UI routes are shown in the sidebar under the parent item.

For example:

```tsx title="src/admin/routes/orders/insights/page.tsx"
// In nested UI route 1 at src/admin/routes/orders/insights/page.tsx
export const config = defineRouteConfig({
  label: "Order Insights",
  nested: "/orders",
  rank: 1, // Will appear first
})

// In nested UI route 2 at src/admin/routes/orders/reports/page.tsx
export const config = defineRouteConfig({
  label: "Order Reports",
  nested: "/orders",
  rank: 2, // Will appear second
})
```

In this example, the "Order Insights" item will appear before the "Order Reports" item under the parent "Orders" item in the sidebar.

***

## Create Settings Page

To create a page under the settings section of the admin dashboard, create a UI route under the path `src/admin/routes/settings`.

For example, create a UI route at `src/admin/routes/settings/custom/page.tsx`:

![Example of settings UI route file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867435/Medusa%20Book/setting-ui-route-dir-overview_kytbh8.jpg)

```tsx title="src/admin/routes/settings/custom/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

const CustomSettingPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h1">Custom Setting Page</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Custom",
})

export default CustomSettingPage
```

This adds a page under the path `/app/settings/custom`. An item is also added to the settings sidebar with the label `Custom`.

***

## Path Parameters

A UI route can accept path parameters if the name of any of the directories in its path is of the format `[param]`.

For example, create the file `src/admin/routes/custom/[id]/page.tsx` with the following content:

![Example of UI route file with path parameters in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867748/Medusa%20Book/path-param-ui-route-dir-overview_kcfbev.jpg)

```tsx title="src/admin/routes/custom/[id]/page.tsx" highlights={[["5", "", "Retrieve the path parameter."], ["10", "{id}", "Show the path parameter."]]}
import { useParams } from "react-router-dom"
import { Container, Heading } from "@medusajs/ui"

const CustomPage = () => {
  const { id } = useParams()

  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h1">Passed ID: {id}</Heading>
      </div>
    </Container>
  )
}

export default CustomPage
```

You access the passed parameter using `react-router-dom`'s [useParams hook](https://reactrouter.com/en/main/hooks/use-params).

If you run the Medusa application and go to `http://localhost:9000/app/custom/123`, you'll see `123` printed in the page.

***

## Set UI Route Breadcrumbs

The Medusa Admin dashboard shows breadcrumbs at the top of each page, if specified. This allows users to navigate through your custom UI routes.

To set the breadcrumbs of a UI route, export a `handle` object with a `breadcrumb` property in the UI route's file:

```tsx title="src/admin/routes/custom/page.tsx" highlights={[["16", "breadcrumb", "Set the breadcrumbs of the UI route."]]}
import { Container, Heading } from "@medusajs/ui"

const CustomPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">This is my custom route</Heading>
      </div>
    </Container>
  )
}

export default CustomPage

export const handle = {
  breadcrumb: () => "Custom Route",
}
```

The `breadcrumb`'s value is a function that returns the breadcrumb label as a string, or a React JSX element.

### Set Breadcrumbs for Nested UI Routes

If you set a breadcrumb for a nested UI route, and you open the route in the Medusa Admin, you'll see the breadcrumbs starting from its parent route to the nested route.

For example, if you have the following UI route at `src/admin/routes/custom/nested/page.tsx` that's nested under the previous one:

```tsx title="src/admin/routes/custom/nested/page.tsx" highlights={[["16", "breadcrumb", "Set the breadcrumbs of the nested UI route."]]}
import { Container, Heading } from "@medusajs/ui"

const NestedCustomPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">This is my nested custom route</Heading>
      </div>
    </Container>
  )
}

export default NestedCustomPage

export const handle = {
  breadcrumb: () => "Nested Custom Route",
}
```

Then, when you open the nested route at `http://localhost:9000/app/custom/nested`, you'll see the breadcrumbs as `Custom Route > Nested Custom Route`. Each breadcrumb is clickable, allowing users to navigate back to the parent route.

### Set Breadcrumbs Dynamically

In some use cases, you may want to show a dynamic breadcrumb for a UI route. For example, if you have a UI route that displays a brand's details, you can set the breadcrumb to show the brand's name dynamically.

To do that, you can:

1. Define and export a `loader` function in the UI route file that fetches the data needed for the breadcrumb.
2. Receive the data in the `breadcrumb` function and return the dynamic label.

For example, create a UI route at `src/admin/routes/brands/[id]/page.tsx` with the following content:

```tsx title="src/admin/routes/brands/[id]/page.tsx" highlights={dynamicBreadcrumbsHighlights}
import { Container, Heading } from "@medusajs/ui"
import { LoaderFunctionArgs, UIMatch, useLoaderData } from "react-router-dom"
import { sdk } from "../../../lib/sdk"

type BrandResponse = {
  brand: {
    name: string
  }
}

const BrandPage = () => {
  const { brand } = useLoaderData() as Awaited<BrandResponse>

  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">{brand.name}</Heading>
      </div>
    </Container>
  )
}

export default BrandPage

export async function loader({ params }: LoaderFunctionArgs) {
  const { id } = params
  const { brand } = await sdk.client.fetch<BrandResponse>(`/admin/brands/${id}`)

  return {
    brand,
  }
}

export const handle = {
  breadcrumb: (
    { data }: UIMatch<BrandResponse>
  ) => data.brand.name || "Brand",
}
```

In the `loader` function, you retrieve the brands from a custom API route and return them.

Then, in the `handle.breadcrumb` function, you receive `data` prop containing the brand information returned by the `loader` function. You can use this data to return a dynamic breadcrumb label.

When you open the UI route at `http://localhost:9000/app/brands/123`, the breadcrumb will show the brand's name, such as `Acme`.

You also use the `useLoaderData` hook to access the data returned by the `loader` function in the UI route component. Learn more in the [Routing Customizations](https://docs.medusajs.com/learn/fundamentals/admin/routing#fetch-data-with-route-loaders/index.html.md) chapter.

***

## Admin Components List

To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](https://docs.medusajs.com/resources/admin-components/index.html.md) to find common components.

***

## More Routes Customizations

For more customizations related to routes, refer to the [Routing Customizations chapter](https://docs.medusajs.com/learn/fundamentals/admin/routing/index.html.md).


# Admin Widgets

In this chapter, you’ll learn about widgets and how to use them.

## What is an Admin Widget?

The Medusa Admin's pages are customizable for inserting widgets of custom content in pre-defined injection zones. For example, you can add a widget on the product details page that allows admin users to sync products to a third-party service.

You create these widgets as React components that render the content and functionality of the widget.

You can create an admin widget directly in your Medusa application, or in a [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) if you want to share the widget across multiple Medusa applications.

***

## How to Create a Widget?

### Prerequisites

- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md)

You create a widget in a `.tsx` file under the `src/admin/widgets` directory. The file must export:

1. A React component that renders the widget. This will be the file's default export.
2. The widget’s configurations indicating where to insert the widget.

For example, create the file `src/admin/widgets/product-widget.tsx` with the following content:

![Example of widget file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867137/Medusa%20Book/widget-dir-overview_dqsbct.jpg)

```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

// The widget
const ProductWidget = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">Product Widget</Heading>
      </div>
    </Container>
  )
}

// The widget's configurations
export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

In the example above, the widget is injected at the top of a product’s details.

You export the `ProductWidget` component, which displays the heading `Product Widget`. In the widget, you use [Medusa UI](https://docs.medusajs.com/ui/index.html.md) to customize the dashboard with the same components used to build it.

To export the widget's configuration, you use `defineWidgetConfig` from the Admin Extension SDK. It accepts an object as a parameter with the `zone` property, whose value is a string or an array of strings, each being the name of the zone to inject the widget into.

The widget component must be created as an arrow function.

### Test the Widget

To test out the widget, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open a product’s details page. You’ll find your custom widget at the top of the page.

***

## Props Passed to Widgets on Detail Pages

Widgets that are injected into a detail page receive a `data` prop, which is the main data of the details page.

For example, a widget injected into the `product.details.before` zone receives the product's details in the `data` prop:

```tsx title="src/admin/widgets/product-widget.tsx" highlights={detailHighlights}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"
import { 
  DetailWidgetProps, 
  AdminProduct,
} from "@medusajs/framework/types"

// The widget
const ProductWidget = ({ 
  data,
}: DetailWidgetProps<AdminProduct>) => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">
          Product Widget {data.title}
        </Heading>
      </div>
    </Container>
  )
}

// The widget's configurations
export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

The props type is `DetailWidgetProps`, and it accepts as a type argument the expected type of `data`. For the product details page, it's `AdminProduct`.

***

## Injection Zones List

Refer to the [Admin Widget Injection Zones](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md) reference for the full list of injection zones and their props.

***

## Admin Components List

While the Medusa Admin uses the [Medusa UI](https://docs.medusajs.com/ui/index.html.md) components, it also expands on them for styling and design purposes.

To build admin customizations that match the Medusa Admin's designs and layouts, refer to the [Admin Components](https://docs.medusajs.com/resources/admin-components/index.html.md) guide. You'll find components like `Header`, `JSON View`, and more that match the Medusa Admin's design.

***

## Show Widgets Conditionally

In some cases, you may want to show a widget only if certain conditions are met. For example, you may want to show a widget only if the product has a brand.

To prevent the widget from showing, return an empty fragment from the widget component:

```tsx title="src/admin/widgets/product-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"
import { 
  DetailWidgetProps, 
  AdminProduct,
} from "@medusajs/framework/types"

// The widget
const ProductWidget = ({ 
  data,
}: DetailWidgetProps<AdminProduct>) => {
  if (!data.metadata?.brand) {
    return <></> // Don't show the widget if the product has no brand
  }

  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">
          Brand: {data.metadata.brand}
        </Heading>
      </div>
    </Container>
  )
}

// The widget's configurations
export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

In the above example, you return an empty fragment if the product has no brand. Otherwise, you show the brand name in the widget.


# Pass Additional Data to Medusa's API Route

In this chapter, you'll learn how to pass additional data in requests to Medusa's API Route.

## Why Pass Additional Data?

Some of Medusa's API Routes accept an `additional_data` parameter whose type is an object. The API Route passes the `additional_data` to the workflow, which in turn passes it to its hooks.

This is useful when you have a link from your custom module to a Commerce Module, and you want to perform an additional action when a request is sent to an existing API route.

For example, the [Create Product API Route](https://docs.medusajs.com/api/admin#products_postproducts) accepts an `additional_data` parameter. If you have a data model linked to it, you consume the `productsCreated` hook to create a record of the data model using the custom data and link it to the product.

### API Routes Accepting Additional Data

### API Routes List

- Campaigns
  - [Create Campaign](https://docs.medusajs.com/api/admin#campaigns_postcampaigns)
  - [Update Campaign](https://docs.medusajs.com/api/admin#campaigns_postcampaignsid)
- Cart
  - [Create Cart](https://docs.medusajs.com/api/store#carts_postcarts)
  - [Update Cart](https://docs.medusajs.com/api/store#carts_postcartsid)
- Collections
  - [Create Collection](https://docs.medusajs.com/api/admin#collections_postcollections)
  - [Update Collection](https://docs.medusajs.com/api/admin#collections_postcollectionsid)
- Customers
  - [Create Customer](https://docs.medusajs.com/api/admin#customers_postcustomers)
  - [Update Customer](https://docs.medusajs.com/api/admin#customers_postcustomersid)
  - [Create Address](https://docs.medusajs.com/api/admin#customers_postcustomersidaddresses)
  - [Update Address](https://docs.medusajs.com/api/admin#customers_postcustomersidaddressesaddress_id)
- Draft Orders
  - [Create Draft Order](https://docs.medusajs.com/api/admin#draft-orders_postdraftorders)
- Orders
  - [Complete Orders](https://docs.medusajs.com/api/admin#orders_postordersidcomplete)
  - [Cancel Order's Fulfillment](https://docs.medusajs.com/api/admin#orders_postordersidfulfillmentsfulfillment_idcancel)
  - [Create Shipment](https://docs.medusajs.com/api/admin#orders_postordersidfulfillmentsfulfillment_idshipments)
  - [Create Fulfillment](https://docs.medusajs.com/api/admin#orders_postordersidfulfillments)
- Products
  - [Create Product](https://docs.medusajs.com/api/admin#products_postproducts)
  - [Update Product](https://docs.medusajs.com/api/admin#products_postproductsid)
  - [Create Product Variant](https://docs.medusajs.com/api/admin#products_postproductsidvariants)
  - [Update Product Variant](https://docs.medusajs.com/api/admin#products_postproductsidvariantsvariant_id)
  - [Create Product Option](https://docs.medusajs.com/api/admin#products_postproductsidoptions)
  - [Update Product Option](https://docs.medusajs.com/api/admin#products_postproductsidoptionsoption_id)
- Product Tags
  - [Create Product Tag](https://docs.medusajs.com/api/admin#product-tags_postproducttags)
  - [Update Product Tag](https://docs.medusajs.com/api/admin#product-tags_postproducttagsid)
- Product Types
  - [Create Product Type](https://docs.medusajs.com/api/admin#product-types_postproducttypes)
  - [Update Product Type](https://docs.medusajs.com/api/admin#product-types_postproducttypesid)
- Promotions
  - [Create Promotion](https://docs.medusajs.com/api/admin#promotions_postpromotions)
  - [Update Promotion](https://docs.medusajs.com/api/admin#promotions_postpromotionsid)

***

## How to Pass Additional Data

### 1. Specify Validation of Additional Data

Before passing custom data in the `additional_data` object parameter, you must specify validation rules for the allowed properties in the object.

To do that, use the middleware route object defined in `src/api/middlewares.ts`.

For example, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import { defineMiddlewares } from "@medusajs/framework/http"
import { z } from "zod"

export default defineMiddlewares({
  routes: [
    {
      method: "POST",
      matcher: "/admin/products",
      additionalDataValidator: {
        brand: z.string().optional(),
      },
    },
  ],
})
```

The middleware route object accepts an optional parameter `additionalDataValidator` whose value is an object of key-value pairs. The keys indicate the name of accepted properties in the `additional_data` parameter, and the value is [Zod](https://zod.dev/) validation rules of the property.

In this example, you indicate that the `additional_data` parameter accepts a `brand` property whose value is an optional string.

Refer to [Zod's documentation](https://zod.dev) for all available validation rules.

### 2. Pass the Additional Data in a Request

You can now pass a `brand` property in the `additional_data` parameter of a request to the Create Product API Route.

For example:

```bash
curl -X POST 'http://localhost:9000/admin/products' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data '{
    "title": "Product 1",
    "options": [
      {
        "title": "Default option",
        "values": ["Default option value"]
      }
    ],
    "shipping_profile_id": "{shipping_profile_id}",
    "additional_data": {
        "brand": "Acme"
    }
}'
```

Make sure to replace the `{token}` in the authorization header with an admin user's authentication token, and `{shipping_profile_id}` with an existing shipping profile's ID.

In this request, you pass in the `additional_data` parameter a `brand` property and set its value to `Acme`.

The `additional_data` is then passed to hooks in the `createProductsWorkflow` used by the API route.

***

## Use Additional Data in a Hook

Learn about workflow hooks in [this guide](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md).

Step functions consuming the workflow hook can access the `additional_data` in the first parameter.

For example, consider you want to store the data passed in `additional_data` in the product's `metadata` property.

To do that, create the file `src/workflows/hooks/product-created.ts` with the following content:

```ts title="src/workflows/hooks/product-created.ts"
import { StepResponse } from "@medusajs/framework/workflows-sdk"
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
import { Modules } from "@medusajs/framework/utils"

createProductsWorkflow.hooks.productsCreated(
  async ({ products, additional_data }, { container }) => {
    if (!additional_data?.brand) {
      return
    }

    const productModuleService = container.resolve(
      Modules.PRODUCT
    )

    await productModuleService.upsertProducts(
      products.map((product) => ({
        ...product,
        metadata: {
          ...product.metadata,
          brand: additional_data.brand,
        },
      }))
    )

    return new StepResponse(products, {
      products,
      additional_data,
    })
  }
)
```

This consumes the `productsCreated` hook, which runs after the products are created.

If `brand` is passed in `additional_data`, you resolve the Product Module's main service and use its `upsertProducts` method to update the products, adding the brand to the `metadata` property.

### Compensation Function

Hooks also accept a compensation function as a second parameter to undo the actions made by the step function.

For example, pass the following second parameter to the `productsCreated` hook:

```ts title="src/workflows/hooks/product-created.ts"
createProductsWorkflow.hooks.productsCreated(
  async ({ products, additional_data }, { container }) => {
    // ...
  },
  async ({ products, additional_data }, { container }) => {
    if (!additional_data.brand) {
      return
    }

    const productModuleService = container.resolve(
      Modules.PRODUCT
    )

    await productModuleService.upsertProducts(
      products
    )
  }
)
```

This updates the products to their original state before adding the brand to their `metadata` property.


# Handling CORS in API Routes

In this chapter, you’ll learn about the CORS middleware and how to configure it for custom API routes.

## CORS Overview

Cross-Origin Resource Sharing (CORS) allows only configured origins to access your API Routes.

For example, if you allow only origins starting with `http://localhost:7001` to access your Admin API Routes, other origins accessing those routes get a CORS error.

### CORS Configurations

The `storeCors` and `adminCors` properties of Medusa's `http` configuration set the allowed origins for routes starting with `/store` and `/admin` respectively.

These configurations accept a URL pattern to identify allowed origins.

For example:

```js title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      storeCors: "http://localhost:8000",
      adminCors: "http://localhost:7001",
      // ...
    },
  },
})
```

This allows the `http://localhost:7001` origin to access the Admin API Routes, and the `http://localhost:8000` origin to access Store API Routes.

Learn more about the CORS configurations in [this resource guide](https://docs.medusajs.com/learn/configurations/medusa-config#http/index.html.md).

***

## CORS in Store and Admin Routes

To disable the CORS middleware for a route, export a `CORS` variable in the route file with its value set to `false`.

For example:

```ts title="src/api/store/custom/route.ts" highlights={[["15"]]}
import type { 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: "[GET] Hello world!",
  })
}

export const CORS = false
```

This disables the CORS middleware on API Routes at the path `/store/custom`.

***

## CORS in Custom Routes

If you create a route that doesn’t start with `/store` or `/admin`, you must apply the CORS middleware manually. Otherwise, all requests to your API route lead to a CORS error.

You can do that in the exported middlewares configurations in `src/api/middlewares.ts`.

For example:

```ts title="src/api/middlewares.ts" highlights={highlights} collapsibleLines="1-10" expandButtonLabel="Show Imports"
import { defineMiddlewares } from "@medusajs/framework/http"
import type { 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse, 
} from "@medusajs/framework/http"
import { ConfigModule } from "@medusajs/framework/types"
import { parseCorsOrigins } from "@medusajs/framework/utils"
import cors from "cors"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      middlewares: [
        (
          req: MedusaRequest, 
          res: MedusaResponse, 
          next: MedusaNextFunction
        ) => {
          const configModule: ConfigModule =
            req.scope.resolve("configModule")

          return cors({
            origin: parseCorsOrigins(
              configModule.projectConfig.http.storeCors
            ),
            credentials: true,
          })(req, res, next)
        },
      ],
    },
  ],
})
```

This retrieves the configurations exported from `medusa-config.ts` and applies the `storeCors` to routes starting with `/custom`.


# Throwing and Handling Errors

In this guide, you'll learn how to throw errors in your Medusa application, how it affects an API route's response, and how to change the default error handler of your Medusa application.

## Throw MedusaError

When throwing an error in your API routes, middlewares, workflows, or any customization, throw a `MedusaError` from the Medusa Framework.

The Medusa application's API route error handler then wraps your thrown error in a uniform object and returns it in the response.

For example:

```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  if (!req.query.q) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "The `q` query parameter is required."
    )
  }

  // ...
}
```

The `MedusaError` class accepts two parameters in its constructor:

1. The first is the error's type. `MedusaError` has a static property `Types` that you can use. `Types` is an enum whose possible values are explained in the next section.
2. The second is the message to show in the error response.

### Error Object in Response

The error object returned in the response has three properties:

- `type`: The error's type.
- `message`: The error message, if available.
- `code`: A common snake-case code. Its values can be:
  - `invalid_request_error` for the `DUPLICATE_ERROR` type.
  - `api_error` for the `DB_ERROR` type.
  - `invalid_state_error` for the `CONFLICT` error type.
  - `unknown_error` for any unidentified error type.
  - For other error types, this property won't be available unless you provide a code as a third parameter to the `MedusaError` constructor.

### MedusaError Types

|Type|Description|Status Code|
|---|---|---|---|---|
|\`DB\_ERROR\`|Indicates a database error.|\`500\`|
|\`DUPLICATE\_ERROR\`|Indicates a duplicate of a record already exists. For example, when trying to create a customer whose email is registered by another customer.|\`422\`|
|\`INVALID\_ARGUMENT\`|Indicates an error that occurred due to incorrect arguments or other unexpected state.|\`500\`|
|\`INVALID\_DATA\`|Indicates a validation error.|\`400\`|
|\`UNAUTHORIZED\`|Indicates that a user is not authorized to perform an action or access a route.|\`401\`|
|\`NOT\_FOUND\`|Indicates that the requested resource, such as a route or a record, isn't found.|\`404\`|
|\`NOT\_ALLOWED\`|Indicates that an operation isn't allowed.|\`400\`|
|\`CONFLICT\`|Indicates that a request conflicts with another previous or ongoing request. The error message in this case is ignored in favor of a default message.|\`409\`|
|\`PAYMENT\_AUTHORIZATION\_ERROR\`|Indicates an error has occurred while authorizing a payment.|\`422\`|
|Other error types|Any other error type results in an |\`500\`|

***

## Override Error Handler

The `defineMiddlewares` function used to apply middlewares on routes accepts an `errorHandler` in its object parameter. Use it to override the default error handler for API routes.

This error handler will also be used for errors thrown in Medusa's API routes and resources.

For example, create `src/api/middlewares.ts` with the following:

```ts title="src/api/middlewares.ts" collapsibleLines="1-8" expandMoreLabel="Show Imports"
import { 
  defineMiddlewares, 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"

export default defineMiddlewares({
  errorHandler: (
    error: MedusaError | any, 
    req: MedusaRequest, 
    res: MedusaResponse, 
    next: MedusaNextFunction
  ) => {
    res.status(400).json({
      error: "Something happened.",
    })
  },
})
```

The `errorHandler` property's value is a function that accepts four parameters:

1. The error thrown. Its type can be `MedusaError` or any other error type.
2. A request object of type `MedusaRequest`.
3. A response object of type `MedusaResponse`.
4. A function of type `MedusaNextFunction` that executes the next middleware in the stack.

This example overrides Medusa's default error handler with a handler that always returns a `400` status code with the same message.

### Re-Use Default Error Handler

In some use cases, you may not want to override the default error handler but rather perform additional actions as part of the original error handler. For example, you might want to capture the error in a third-party service like Sentry.

In those cases, you can import the default error handler from the Medusa Framework and use it in your custom error handler, along with your custom logic.

For example:

```ts title="src/api/middlewares.ts" highlights={defaultErrorHandlerHighlights} 
import { 
  defineMiddlewares, 
  errorHandler, 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
// assuming you have Sentry set up in your project
import * as Sentry from "@sentry/node"

const originalErrorHandler = errorHandler()

export default defineMiddlewares({
  errorHandler: (
    error: MedusaError | any, 
    req: MedusaRequest, 
    res: MedusaResponse, 
    next: MedusaNextFunction
  ) => {
    // for example, capture the error in Sentry
    Sentry.captureException(error)
    return originalErrorHandler(error, req, res, next)
  },
})
```

In this example, you import the `errorHandler` function from the Medusa Framework. Then, you call it to get the original error handler function.

Finally, you use it in your custom error handler after performing your custom logic, such as capturing the error in Sentry.


# HTTP Methods

In this chapter, you'll learn about how to add new API routes for each HTTP method.

## HTTP Method Handler

An API route is created for every HTTP method you export a handler function for in a `route.ts` or `route.js` file.

Allowed HTTP methods are: `GET`, `POST`, `DELETE`, `PUT`, `PATCH`, `OPTIONS`, and `HEAD`.

For example, create the file `src/api/hello-world/route.ts` with the following content:

```ts title="src/api/hello-world/route.ts"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: "[GET] Hello world!",
  })
}

export const POST = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: "[POST] Hello world!",
  })
}
```

This file adds two API Routes:

- A `GET` API route at `http://localhost:9000/hello-world`.
- A `POST` API route at `http://localhost:9000/hello-world`.


# Localization in API Routes

In this chapter, you'll learn how to handle localization in API routes of your Medusa application to serve content in different languages.

### Prerequisites

- [Medusa v2.12.4 or later](https://github.com/medusajs/medusa/releases/tag/v2.12.4)
- [Translation Module Configured](https://docs.medusajs.com/resources/commerce-modules/translation#configure-translation-module/index.html.md)

## Overview

Localization in API routes allows you to serve translated content based on the user's preferred language. The Medusa application provides built-in support for handling locale information in API requests and retrieving localized data.

When a locale is specified in a request, you can use it to retrieve translated versions of your data models' fields, providing a seamless multilingual experience for your users.

Learn more about translation, how to manage translations, and how to translate custom data models in the [Translation Module documentation](https://docs.medusajs.com/resources/commerce-modules/translation/index.html.md).

***

## Routes with Localization Enabled by Default

The Medusa application automatically supports retrieving localized content from all routes under the `/store` prefix, including both core and custom store API routes.

For example, the following store routes have localization enabled by default:

- `/store/products` -> Get products with translated fields
- `/store/collections` -> Get collections with translated fields
- `/store/categories` -> Get categories with translated fields

Refer to the [Translation Module](https://docs.medusajs.com/resources/commerce-modules/translation#supported-module-translations/index.html.md) documentation for a list of supported core data models with localization support.

### Apply Localization to Custom Routes

If you're creating custom API routes outside the `/store` prefix, you must manually apply the `applyLocale` [middleware](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) to enable localization support.

To apply the `applyLocale` middleware to all HTTP methods for a route, add it to the `src/api/middlewares.ts` file:

```ts title="src/api/middlewares.ts" highlights={allMethodsHighlights}
import { applyLocale, defineMiddlewares } from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      middlewares: [applyLocale],
    },
  ],
})
```

This applies the `applyLocale` middleware to all routes matching `/custom*`, regardless of the HTTP method.

Alternatively, you can apply the middleware only to specific HTTP methods using the `method` property:

```ts title="src/api/middlewares.ts" highlights={specificMethodHighlights}
import { applyLocale, defineMiddlewares } from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      method: ["GET"],
      middlewares: [applyLocale],
    },
  ],
})
```

Learn more about middlewares in the [Middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) chapter.

***

## How to Pass Locale in API Requests

You can pass the locale in API requests to routes that support localization using either of the following methods:

1. The `locale` query parameter
2. The `x-medusa-locale` request header

The query parameter takes priority over the header if both are provided.

The locale must follow the [IETF BCP 47 standard](https://gist.github.com/typpo/b2b828a35e683b9bf8db91b5404f1bd1), such as `en-US` for English (United States) or `fr-FR` for French (France).

Refer to the [JS SDK reference](https://docs.medusajs.com/resources/js-sdk#localization-with-js-sdk/index.html.md) for details on how to pass locale.

For example:

### Query Parameter

```bash
curl "http://localhost:9000/store/products?locale=fr-FR" \
-H 'x-publishable-api-key: {your_publishable_api_key}'
```

### Header

```bash
curl "http://localhost:9000/store/products" \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
-H 'x-medusa-locale: fr-FR'
```

The above examples retrieve products with their fields translated to French (France) if translations are available. If no translations exist for the requested locale, the original content stored in the data model is returned.

Store API routes require a publishable API key in the request header. Learn more in the [Store API reference](https://docs.medusajs.com/api/store#publishable-api-key).

***

## Access Request Locale in API Routes

After applying the `applyLocale` middleware, you can access the request's locale from the `locale` property of the `MedusaRequest` object.

For example:

```ts title="src/api/custom/route.ts" highlights={accessLocaleHighlights}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const locale = req.locale

  // use locale to retrieve localized data...
}
```

The `req.locale` property contains the locale value from either the query parameter or the request header. If no locale is specified in the request, `req.locale` is `undefined`.

### Retrieve Localized Data with Query

To retrieve data models with translated fields, pass the `locale` property in the second parameter object of [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) when querying your data.

For example, to retrieve products with translated names and descriptions:

```ts title="src/api/store/products/route.ts" highlights={queryHighlights}
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
  const query = req.scope.resolve("query")

  const { data: products } = await query.graph(
    {
      entity: "product",
      fields: ["id", "title", "description"],
    },
    {
      locale: req.locale,
    }
  )

  res.json({ products })
}
```

In this example, the products are retrieved with their `title` and `description` fields translated to the locale specified in the request.

Learn more in the [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query#retrieve-localized-data/index.html.md) chapter.

### Retrieve Localized Data for Custom Models

You can also retrieve localized data for custom data models. Learn more in the [Translate Custom Data Models](https://docs.medusajs.com/resources/commerce-modules/translation/custom-data-models/index.html.md) guide.


# Middlewares

In this chapter, you’ll learn about middlewares and how to create them.

## What is a Middleware?

A middleware is a function executed when a request is sent to an API Route. It's executed before the route handler function.

Middlewares are used to guard API routes, parse request content types other than `application/json`, manipulate request data, and more.

![API middleware execution flow diagram showing how HTTP requests first pass through middleware functions for authentication, validation, and data processing before reaching the actual route handler, providing a secure and flexible request processing pipeline in Medusa applications](https://res.cloudinary.com/dza7lstvk/image/upload/v1746775148/Medusa%20Book/middleware-overview_wc2ws5.jpg)

As Medusa's server is based on Express, you can use any [Express middleware](https://expressjs.com/en/resources/middleware.html).

### Middleware Types

There are two types of middlewares:

|Type|Description|Example|
|---|---|---|
|Global Middleware|A middleware that applies to all routes matching a specified pattern.|\`/custom\*\`|
|Route Middleware|A middleware that applies to routes matching a specified pattern and HTTP method(s).|A middleware that applies to all |

These middlewares generally have the same definition and usage, but they differ in the routes they apply to. You'll learn how to create both types in the following sections.

***

## How to Create a Middleware?

Middlewares of all types are defined in the special file `src/api/middlewares.ts`. Use the `defineMiddlewares` function from the Medusa Framework to define the middlewares, and export its value.

For example:

### Global Middleware

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse, 
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      middlewares: [
        (
          req: MedusaRequest, 
          res: MedusaResponse, 
          next: MedusaNextFunction
        ) => {
          console.log("Received a request!")

          next()
        },
      ],
    },
  ],
})
```

### Route Middleware

```ts title="src/api/middlewares.ts" highlights={highlights}
import { 
  defineMiddlewares,
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse, 
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      method: ["POST", "PUT"],
      middlewares: [
        (
          req: MedusaRequest, 
          res: MedusaResponse, 
          next: MedusaNextFunction
        ) => {
          console.log("Received a request!")

          next()
        },
      ],
    },
  ],
})
```

The `defineMiddlewares` function accepts a middleware configurations object that has the property `routes`. `routes`'s value is an array of middleware route objects, each having the following properties:

- `matcher`: a string or regular expression indicating the API route path to apply the middleware on. The regular expression must be compatible with [path-to-regexp](https://github.com/pillarjs/path-to-regexp).
- `middlewares`: An array of global and route middleware functions.
- `method`: (optional) By default, a middleware is applied on all HTTP methods for a route. You can specify one or more HTTP methods to apply the middleware to in this option, making it a route middleware.

### Test the Middleware

To test the middleware:

1. Start the application:

```bash npm2yarn
npm run dev
```

2. Send a request to any API route starting with `/custom`. If you specified an HTTP method in the `method` property, make sure to use that method.
3. See the following message in the terminal:

```bash
Received a request!
```

### Troubleshooting

If the middleware didn't run, make sure you've correctly created the middleware at `src/api/middlewares.ts` (with correct spelling). This is a common mistake that can lead to the middleware not being applied, resulting in unexpected behavior.

***

## When to Use Middlewares

Middlewares are useful for:

- [Protecting API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/protected-routes/index.html.md) to ensure that only authenticated users can access them.
- [Validating](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md) request query and body parameters.
- [Parsing](https://docs.medusajs.com/learn/fundamentals/api-routes/parse-body/index.html.md) request content types other than `application/json`.
- [Applying CORS](https://docs.medusajs.com/learn/fundamentals/api-routes/cors/index.html.md) configurations to custom API routes.

***

## Middleware Function Parameters

The middleware function accepts three parameters:

1. A request object of type `MedusaRequest`.
2. A response object of type `MedusaResponse`.
3. A function of type `MedusaNextFunction` that executes the next middleware in the stack.

You must call the `next` function in the middleware. Otherwise, other middlewares and the API route handler won’t execute.

For example:

```ts title="src/api/middlewares.ts"
import { 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse, 
  defineMiddlewares,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      middlewares: [
        (
          req: MedusaRequest, 
          res: MedusaResponse, 
          next: MedusaNextFunction
        ) => {
          console.log("Received a request!", req.body)

          next()
        },
      ],
    },
  ],
})
```

This middleware logs the request body to the terminal, then calls the `next` function to execute the next middleware in the stack.

***

## Middleware for Routes with Path Parameters

To indicate a path parameter in a middleware's `matcher` pattern, use the format `:{param-name}`.

A middleware applied on a route with path parameters is a route middleware.

For example:

```ts title="src/api/middlewares.ts" collapsibleLines="1-7" expandMoreLabel="Show Imports" highlights={pathParamHighlights}
import { 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse, 
  defineMiddlewares,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom/:id",
      middlewares: [
        // ...
      ],
    },
  ],
})
```

This applies a middleware to the routes defined in the file `src/api/custom/[id]/route.ts`.

***

## Request URLs with Trailing Backslashes

A middleware whose `matcher` pattern doesn't end with a backslash won't be applied for requests to URLs with a trailing backslash.

For example, consider you have the following middleware:

```ts title="src/api/middlewares.ts" collapsibleLines="1-7" expandMoreLabel="Show Imports"
import { 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse, 
  defineMiddlewares,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom",
      middlewares: [
        (
          req: MedusaRequest, 
          res: MedusaResponse, 
          next: MedusaNextFunction
        ) => {
          console.log("Received a request!")

          next()
        },
      ],
    },
  ],
})
```

If you send a request to `http://localhost:9000/custom`, the middleware will run.

However, if you send a request to `http://localhost:9000/custom/`, the middleware won't run.

In general, avoid adding trailing backslashes when sending requests to API routes.

***

## How Are Middlewares Ordered and Applied?

The information explained in this section is applicable starting from [Medusa v2.6](https://github.com/medusajs/medusa/releases/tag/v2.6).

### Middleware and Routes Execution Order

The Medusa application registers middlewares and API route handlers in the following order, stacking them on top of each other:

![Diagram showcasing the order in which middlewares and route handlers are registered.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746776911/Medusa%20Book/middleware-registration-overview_spc02f.jpg)

1. Global middlewares in the following order:
   1. Global middleware defined in the Medusa's core.
   2. Global middleware defined in the plugins (in the order the plugins are registered in).
   3. Global middleware you define in the application.
2. Route middlewares in the following order:
   1. Route middleware defined in the Medusa's core.
   2. Route middleware defined in the plugins (in the order the plugins are registered in).
   3. Route middleware you define in the application.
3. API routes in the following order:
   1. API routes defined in the Medusa's core.
   2. API routes defined in the plugins (in the order the plugins are registered in).
   3. API routes you define in the application.

Then, when a request is sent to an API route, the stack is executed in order: global middlewares are executed first, then the route middlewares, and finally the route handlers.

![Diagram showcasing the order in which middlewares and route handlers are executed when a request is sent to an API route.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746776172/Medusa%20Book/middleware-order-overview_h7kzfl.jpg)

For example, consider you have the following middlewares:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom",
      middlewares: [
        (req, res, next) => {
          console.log("Global middleware")
          next()
        },
      ],
    },
    {
      matcher: "/custom",
      method: ["GET"],
      middlewares: [
        (req, res, next) => {
          console.log("Route middleware")
          next()
        },
      ],
    },
  ],
})
```

When you send a request to `/custom` route, the following messages are logged in the terminal:

```bash
Global middleware
Route middleware
Hello from custom! # message logged from API route handler
```

The global middleware runs first, then the route middleware, and finally the route handler, assuming that it logs the message `Hello from custom!`.

### Middlewares Sorting

On top of the previous ordering, Medusa sorts global and route middlewares based on their matcher pattern in the following order:

1. Wildcard matchers. For example, `/custom*`.
2. Regex matchers. For example, `/custom/(products|collections)`.
3. Static matchers without parameters. For example, `/custom`.
4. Static matchers with parameters. For example, `/custom/:id`.

For example, if you have the following middlewares:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom/:id",
      middlewares: [/* ... */],
    },
    {
      matcher: "/custom",
      middlewares: [/* ... */],
    },
    {
      matcher: "/custom*",
      method: ["GET"],
      middlewares: [/* ... */],
    },
    {
      matcher: "/custom/:id",
      method: ["GET"],
      middlewares: [/* ... */],
    },
  ],
})
```

The global middlewares are sorted into the following order before they're registered:

1. Global middleware `/custom`.
2. Global middleware `/custom/:id`.

And the route middlewares are sorted into the following order before they're registered:

1. Route middleware `/custom*`.
2. Route middleware `/custom/:id`.

![Diagram showcasing the order in which middlewares are sorted before being registered.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746777297/Medusa%20Book/middleware-registration-sorting_oyfqhw.jpg)

Then, the middlwares are registered in the order mentioned earlier, with global middlewares first, then the route middlewares.

***

## Overriding Middlewares

A middleware can not override an existing middleware. Instead, middlewares are added to the end of the middleware stack.

For example, if you define a custom validation middleware, such as `validateAndTransformBody`, on an existing route, then both the original and the custom validation middleware will run.

Similarly, if you add an [authenticate](https://docs.medusajs.com/learn/fundamentals/api-routes/protected-routes#protect-custom-api-routes/index.html.md) middleware to an existing route, both the original and the custom authentication middleware will run. So, you can't override the original authentication middleware.

### Alternative Solution to Overriding Middlewares

If you need to change the middlewares applied to a route, you can create a custom [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) that executes the same functionality as the original route, but with the middlewares you want.

Learn more in the [Override API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/override/index.html.md) chapter.


# Override API Routes

In this chapter, you'll learn the approach recommended when you need to override an existing API route in Medusa.

## Approaches to Consider Instead of Overriding API Routes

While building customizations in your Medusa application, you may need to make changes to existing API routes for your business use case.

Medusa provides the following approaches to customize API routes:

|Approach|Description|
|---|---|
|Pass Additional Data|Pass custom data to the API route with custom validation.|
|Perform Custom Logic within an Existing Flows|API routes execute workflows to perform business logic, which may have hooks that allow you to perform custom logic.|
|Use Custom Middlewares|Use custom middlewares to perform custom logic before the API route is executed. However, you cannot remove or replace middlewares applied to existing API routes.|
|Listen to Events in Subscribers|Functionalities in API routes may trigger events that you can handle in subscribers. This is useful if you're performing an action that isn't integral to the API route's core functionality or response.|

If the above approaches do not meet your needs, you can consider the approaches mentioned in the rest of this chapter.

***

## Replicate, Don't Override API Routes

If the approaches mentioned in the [section above](#approaches-to-consider-before-overriding-api-routes) do not meet your needs, you can replicate an existing API route and modify it to suit your requirements.

By replicating instead of overriding, the original API route remains intact, allowing you to easily revert to the original functionality if needed. You can also update your Medusa version without worrying about breaking changes in the original API route.

***

## How to Replicate an API Route?

Medusa's API routes are generally slim and use logic contained in [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). So, creating a custom route based on the original route is straightforward.

You can view the source code for Medusa's API routes in the [Medusa GitHub repository](https://github.com/medusajs/medusa/tree/develop/packages/medusa/src/api).

For example, if you need to allow vendors to access the `POST /admin/products` API route, you can create an API route in your Medusa project at `src/api/vendor/products/route.ts` with the [same code as the original route](https://github.com/medusajs/medusa/blob/develop/packages/medusa/src/api/admin/products/route.ts#L88). Then, you can make changes to it or its middlewares.

***

## When to Replicate an API Route?

Some examples of when you might want to replicate an API route include:

|Use Case|Description|
|---|---|
|Custom Validation|You want to change the validation logic for a specific API route, and the |
|Change Authentication|You want to remove required authentication for a specific API route, or you want to allow custom |
|Custom Response|You want to change the response format of an existing API route.|
|Override Middleware|You want to override the middleware applied on existing API routes. Because of |


# API Routes

In this chapter, you’ll learn what API routes are and how to create them.

## What is an API Route?

An API route is a REST endpoint that exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems.

The Medusa core application provides a set of [admin](https://docs.medusajs.com/api/admin) and [store](https://docs.medusajs.com/api/store) API routes out-of-the-box. You can also create custom API routes to expose your custom functionalities.

***

## How to Create an API Route?

You can create an API route in a TypeScript or JavaScript file under the `src/api` directory of your Medusa application. The file’s name must be `route.ts` or `route.js`.

![Example of API route in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732808645/Medusa%20Book/route-dir-overview_dqgzmk.jpg)

Each file exports API route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…).

For example, to create a `GET` API route at `/hello-world`, create the file `src/api/hello-world/route.ts` with the following content:

```ts title="src/api/hello-world/route.ts"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: "[GET] Hello world!",
  })
}
```

### Test the API Route

To test the API route above, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, send a `GET` request to the `/hello-world` API route:

```bash
curl http://localhost:9000/hello-world
```

You should receive the following response:

```json
{
  "message": "[GET] Hello world!"
}
```

***

## Next Chapters: Learn More About API Routes

Follow the next chapters to learn about the different HTTP methods you can use in API routes, parameters and validation, protecting routes, and more.


# API Route Parameters

In this chapter, you’ll learn about path, query, and request body parameters.

## Path Parameters

To create an API route that accepts a path parameter, create a directory within the route file's path whose name is of the format `[param]`.

For example, to create an API Route at the path `/hello-world/:id`, where `:id` is a path parameter, create the file `src/api/hello-world/[id]/route.ts` with the following content:

```ts title="src/api/hello-world/[id]/route.ts" highlights={singlePathHighlights}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: `[GET] Hello ${req.params.id}!`,
  })
}
```

The `MedusaRequest` object has a `params` property. `params` holds the path parameters in key-value pairs.

### Multiple Path Parameters

To create an API route that accepts multiple path parameters, create within the file's path multiple directories whose names are of the format `[param]`.

For example, to create an API route at `/hello-world/:id/name/:name`, create the file `src/api/hello-world/[id]/name/[name]/route.ts` with the following content:

```ts title="src/api/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: `[GET] Hello ${
      req.params.id
    } - ${req.params.name}!`,
  })
}
```

You access the `id` and `name` path parameters using the `req.params` property.

***

## Query Parameters

You can access all query parameters in the `query` property of the `MedusaRequest` object. `query` is an object of key-value pairs, where the key is a query parameter's name, and the value is its value.

For example:

```ts title="src/api/hello-world/route.ts" highlights={queryHighlights}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: `Hello ${req.query.name}`,
  })
}
```

The value of `req.query.name` is the value passed in `?name=John`, for example.

### Validate Query Parameters

You can apply validation rules on received query parameters to ensure they match specified rules and types.

Learn more in the [API Route Validation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-query-parameters/index.html.md) chapter.

***

## Request Body Parameters

The Medusa application parses the body of any request with JSON, URL-encoded, or text content types. The request body parameters are set in the `MedusaRequest`'s `body` property.

Learn more about configuring body parsing in the [Body Parsing](https://docs.medusajs.com/learn/fundamentals/api-routes/parse-body/index.html.md) chapter.

For example:

```ts title="src/api/hello-world/route.ts" highlights={bodyHighlights}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

type HelloWorldReq = {
  name: string
}

export const POST = async (
  req: MedusaRequest<HelloWorldReq>,
  res: MedusaResponse
) => {
  res.json({
    message: `[POST] Hello ${req.body.name}!`,
  })
}
```

In this example, you use the `name` request body parameter to create the message in the returned response.

The `MedusaRequest` type accepts a type argument that indicates the type of the request body. This is useful for auto-completion and to avoid typing errors.

To test it out, send the following request to your Medusa application:

```bash
curl -X POST 'http://localhost:9000/hello-world' \
-H 'Content-Type: application/json' \
--data-raw '{
  "name": "John"
}'
```

This returns the following JSON object:

```json
{
  "message": "[POST] Hello John!"
}
```

### Validate Body Parameters

You can apply validation rules on received body parameters to ensure they match specified rules and types.

Learn more in the [Body Validation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-body/index.html.md) chapter.


# Configure Request Body Parser

In this chapter, you'll learn how to configure the request body parser for your API routes.

## Default Body Parser Configuration

The Medusa application configures the body parser by default to parse JSON, URL-encoded, and text request content types. You can parse other data types by adding the relevant [Express middleware](https://expressjs.com/en/guide/using-middleware.html), or preserve the raw body data by configuring the body parser, which is useful for webhook requests.

This chapter provides examples of configuring the body parser for different data types or use cases.

***

## Preserve Raw Body Data for Webhooks

If your API route receives webhook requests, you might want to preserve the raw body data. To do this, you can configure the body parser to parse the raw body data and store it in the `req.rawBody` property.

To do this, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" highlights={preserveHighlights}
import { defineMiddlewares } from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      method: ["POST"],
      bodyParser: { preserveRawBody: true },
      matcher: "/custom",
    },
  ],
})
```

The middleware route object passed to `routes` accepts a `bodyParser` property, whose value is a configuration object for the default body parser. By enabling the `preserveRawBody` property, the raw body data is preserved and stored in the `req.rawBody` property.

Learn more about [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md).

You can then access the raw body data in your API route handler:

```ts title="src/api/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export async function POST(
  req: MedusaRequest,
  res: MedusaResponse
) {
  console.log(req.rawBody)

  // TODO use raw body
}
```

***

## Configure Request Body Size Limit

By default, the body parser limits the request body size to `100kb`. If a request body exceeds that size, the Medusa application throws an error.

You can configure the body parser to accept larger request bodies by setting the `sizeLimit` property of the `bodyParser` object in a middleware route object. For example:

```ts title="src/api/middlewares.ts" highlights={sizeLimitHighlights}
import { defineMiddlewares } from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      method: ["POST"],
      bodyParser: { sizeLimit: "2mb" },
      matcher: "/custom",
    },
  ],
})
```

The `sizeLimit` property accepts one of the following types of values:

- A string representing the size limit (for example, `100kb`, `2mb`, `5gb`). It is passed to the [bytes](https://www.npmjs.com/package/bytes) library to parse the size.
- A number representing the size limit in bytes. For example, `1024` for 1kb.

### Overriding Request Body Size Limit

You can't override the request body size limit for existing routes, such as Medusa's Store and Admin API routes, due to how [middlewares are applied](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares#how-are-middlewares-ordered-and-applied/index.html.md).

If you need to override the request body size limit for a specific route, you can create a custom [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) that executes the same functionality as the original route, but with the body size limit you need.

Learn more in the [Override API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/override/index.html.md) chapter.

***

## Configure File Uploads

To accept file uploads in your API routes, you can configure the [Express Multer middleware](https://expressjs.com/en/resources/middleware/multer.html) on your route.

The `multer` package is available through the `@medusajs/medusa` package, so you don't need to install it. However, for better TypeScript support, install the `@types/multer` package as a development dependency:

```bash npm2yarn
npm install --save-dev @types/multer
```

Then, to configure file uploads for your route, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" highlights={uploadHighlights}
import { defineMiddlewares } from "@medusajs/framework/http"
import multer from "multer"

const upload = multer({ storage: multer.memoryStorage() })

export default defineMiddlewares({
  routes: [
    {
      method: ["POST"],
      matcher: "/custom",
      middlewares: [
        // @ts-ignore
        upload.array("files"),
      ],
    },
  ],
})
```

In the example above, you configure the `multer` middleware to store the uploaded files in memory.

Then, you apply the `upload.array("files")` middleware to the route to accept file uploads. By using the `array` method, you accept multiple file uploads with the same `files` field name.

You can then access the uploaded files in your API route handler:

```ts title="src/api/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
import { uploadFilesWorkflow } from "@medusajs/medusa/core-flows"

export async function POST(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const files = req.files as Express.Multer.File[]

  if (!files?.length) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "No files were uploaded"
    )
  }

  const { result } = await uploadFilesWorkflow(req.scope).run({
    input: {
      files: files?.map((f) => ({
        filename: f.originalname,
        mimeType: f.mimetype,
        content: f.buffer.toString("binary"),
        access: "public",
      })),
    },
  })

  res.status(200).json({ files: result })
}
```

The uploaded files are stored in the `req.files` property as an array of Multer file objects that have properties like `filename` and `mimetype`.

To upload the files, you can use the [uploadFilesWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/uploadFilesWorkflow/index.html.md), which uploads the files using the configured [File Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/file/index.html.md).

### Test API Route

To test the API route, send a `POST` request to the `/custom` endpoint with the `files` field containing one or more files:

```bash
curl -X POST 'localhost:9000/custom' \
--form 'files=@"/path/to/file.png"'
```

Where `/path/to/file.png` is the path to the file you want to upload.

The request will return a JSON response with the uploaded file information:

```json title="Example Response"
{
  "files": [
    {
      "id": "file_1",
      "url": "https://example.com/files/file_1"
    }
  ]
}
```


# Protected API Routes

In this chapter, you’ll learn how to create protected API routes.

## What is a Protected API Route?

By default, an API route is publicly accessible, meaning that any user can access it without authentication. This is useful for public API routes that allow users to browse products, view collections, and so on.

A protected API route is an API route that requires requests to be user-authenticated before performing the route's functionality. Otherwise, the request fails, and the user is prevented access.

Protected API routes are useful for routes that require user authentication, such as creating a product or managing an order. These routes must only be accessed by authenticated admin users.

Refer to the API Reference for [Admin](https://docs.medusajs.com/api/admin#authentication) and [Store](https://docs.medusajs.com/api/store#authentication) to learn how to send authenticated requests.

***

## Default Protected Routes

Any API route, including your custom API routes, are protected if they start with the following prefixes:

|Route Prefix|Access|
|---|---|
|\`/admin\`|Only authenticated admin users can access.|
|\`/store/customers/me\`|Only authenticated customers can access.|

Refer to the API Reference for [Admin](https://docs.medusajs.com/api/admin#authentication) and [Store](https://docs.medusajs.com/api/store#authentication) to learn how to send authenticated requests.

### Opt-Out of Default Authentication Requirement

If you create a custom API route under a prefix that is protected by default, you can opt-out of the authentication requirement by exporting an `AUTHENTICATE` variable in the route file with its value set to `false`.

For example, to disable authentication requirement for a custom API route created at `/admin/custom`, you can export an `AUTHENTICATE` variable in the route file:

```ts title="src/api/admin/custom/route.ts" highlights={[["15"]]}
import type { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: AuthenticatedMedusaRequest, 
  res: MedusaResponse
) => {
  res.json({
    message: "Hello",
  })
}

export const AUTHENTICATE = false
```

Now, any request sent to the `/admin/custom` API route is allowed, regardless if the admin user is authenticated.

***

## Protect Custom API Routes

You can protect API routes using the `authenticate` [middleware](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) from the Medusa Framework. When applied to a route, the middleware checks that:

- The correct actor type (for example, `user`, `customer`, or a custom actor type) is authenticated.
- The correct authentication method is used (for example, `session`, `bearer`, or `api-key`).

For example, you can add the `authenticate` middleware in the `src/api/middlewares.ts` file to protect a custom API route:

```ts title="src/api/middlewares.ts" highlights={highlights}
import { 
  defineMiddlewares,
  authenticate,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom/admin*",
      middlewares: [authenticate("user", ["session", "bearer", "api-key"])],
    },
    {
      matcher: "/custom/customer*",
      middlewares: [authenticate("customer", ["session", "bearer"])],
    },
  ],
})
```

The `authenticate` middleware function accepts three parameters:

1. The type of user authenticating. Use `user` for authenticating admin users, and `customer` for authenticating customers. You can also pass `*` to allow all types of users, or pass an array of actor types.
2. An array of types of authentication methods allowed. Both `user` and `customer` scopes support `session` and `bearer`. The `admin` scope also supports the `api-key` authentication method.
3. An optional object of configurations accepting the following properties:
   - `allowUnauthenticated`: (default: `false`) A boolean indicating whether authentication is required. For example, you may have an API route where you want to access the logged-in customer if available, but guest customers can still access it too.
   - `allowUnregistered` (default: `false`): A boolean indicating whether users can access this route with a registration token, instead of an authentication token. This is useful if you have a custom actor type, such as `manager`, and you're creating an API route that allows these users to register themselves. Learn more in the [Custom Actor-Type Guide](https://docs.medusajs.com/resources/commerce-modules/auth/create-actor-type/index.html.md).

### Example: Custom Actor Type

For example, to require authentication of a custom actor type `manager` to an API route:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  authenticate,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/manager*",
      middlewares: [authenticate("manager", ["session", "bearer"])],
    },
  ],
})
```

Refer to the [Custom Actor-Type Guide](https://docs.medusajs.com/resources/commerce-modules/auth/create-actor-type/index.html.md) for detailed explanation on how to create a custom actor type and apply authentication middlewares.

### Example: Allow Multiple Actor Types

To allow multiple actor types to access an API route, pass an array of actor types to the `authenticate` middleware:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  authenticate,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      middlewares: [authenticate(["user", "customer"], ["session", "bearer"])],
    },
  ],
})
```

### Override Authentication for Medusa's API Routes

In some cases, you may want to override the authentication requirement for Medusa's API routes. For example, you may want to allow custom actor types to access existing protected API routes.

It's not possible to change the [authentication middleware](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) applied to an existing API route. Instead, you need to replicate the API route and apply the authentication middleware to it.

Learn more in the [Override API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/override/index.html.md) chapter.

***

## Access Authentication Details in API Routes

To access the authentication details in an API route, such as the logged-in user's ID, set the type of the first request parameter to `AuthenticatedMedusaRequest`. It extends `MedusaRequest`:

```ts highlights={[["7", "AuthenticatedMedusaRequest"]]}
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  // ...
}
```

The `auth_context.actor_id` property of `AuthenticatedMedusaRequest` holds the ID of the authenticated user or customer. If there isn't any authenticated user or customer, `auth_context` is `undefined`.

For example:

```ts title="src/api/store/custom/route.ts" highlights={[["10", "actor_id"]]}
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const id = req.auth_context?.actor_id

  // ...
}
```

In this example, you retrieve the ID of the authenticated user, customer, or custom actor type from the `auth_context` property of the `AuthenticatedMedusaRequest` object.

If you opt-out of authentication in a route as mentioned in the [Opt-Out section](#opt-out-of-default-authentication-requirement), you can't access the authenticated user or customer anymore. Use the [authenticate middleware](#protect-custom-api-routes) instead to protect the route.

### Retrieve Logged-In Customer's Details

You can access the logged-in customer’s ID in all API routes starting with `/store` using the `auth_context.actor_id` property of the `AuthenticatedMedusaRequest` object. You can then use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) to retrieve the customer details, or pass the ID to a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that performs business logic.

For example:

```ts title="src/api/store/custom/route.ts" highlights={customerHighlights}
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const customerId = req.auth_context?.actor_id
  const query = req.scope.resolve("query")

  const { data: [customer] } = await query.graph({
    entity: "customer",
    fields: ["*"],
    filters: {
      id: customerId,
    },
  }, {
    throwIfKeyNotFound: true,
  })

  // do something with the customer data...
}
```

In this example, you retrieve the customer's ID and resolve Query from the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md).

Then, you use Query to retrieve the customer details. The `throwIfKeyNotFound` option throws an error if the customer with the specified ID is not found.

After that, you can use the customer's details in your API route.

### Retrieve Logged-In Admin User's Details

You can access the logged-in admin user’s ID in all API routes starting with `/admin` using the `auth_context.actor_id` property of the `AuthenticatedMedusaRequest` object. You can then use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) to retrieve the user details, or pass the ID to a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that performs business logic.

For example:

```ts title="src/api/admin/custom/route.ts" highlights={adminHighlights}
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const userId = req.auth_context?.actor_id
  const query = req.scope.resolve("query")

  const { data: [user] } = await query.graph({
    entity: "user",
    fields: ["*"],
    filters: {
      id: userId,
    },
  }, {
    throwIfKeyNotFound: true,
  })

  // do something with the user data...
}
```

In this example, you retrieve the admin user's ID and resolve Query from the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md).

Then, you use Query to retrieve the user details. The `throwIfKeyNotFound` option throws an error if the user with the specified ID is not found.

After that, you can use the user's details in your API route.


# API Route Response

In this chapter, you'll learn how to send a response in your API route.

## Send a JSON Response

To send a JSON response, use the `json` method of the `MedusaResponse` object that is passed as the second parameter of your API route handler.

For example:

```ts title="src/api/custom/route.ts" highlights={jsonHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: "Hello, World!",
  })
}
```

This API route returns the following JSON object:

```json
{
  "message": "Hello, World!"
}
```

***

## Set Response Status Code

By default, setting the JSON data using the `json` method returns a response with a `200` status code.

To change the status code, use the `status` method of the `MedusaResponse` object.

For example:

```ts title="src/api/custom/route.ts" highlights={statusHighlight}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.status(200).json({
    message: "Hello, World!",
  })
}
```

The response of this API route has the status code `201`.

***

## Change Response Content Type

To return response data other than a JSON object, you can either use the `set`, `setHeader`, or `writeHead` methods of the `MedusaResponse` object. They allow you to set the response headers, including the content type.

### Example: Return PDF File

To create an API route that returns a PDF file, you can set the `Content-Type` header to `application/pdf`:

```ts title="src/api/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  // assuming you have the PDF file as buffer
  res.set({
    "Content-Type": "application/pdf",
    "Content-Disposition": `attachment; filename="invoice-${id}.pdf"`,
    "Content-Length": buffer.length,
  })
  res.send(buffer)
}
```

The `set` method allows you to set multiple headers at once. It accepts an object of key-value header pairs.

### Example: Return XML Content

To create an API route that returns XML content, you can set the `Content-Type` header to `application/xml`:

```ts title="src/api/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  // Assuming you have XML string
  res.set({
    "Content-Type": "application/xml; charset=utf-8",
  })
  res.send(xmlString)
}
```

### Example: Server-Sent Events (SSE)

To create an API route that returns server-sent events (SSE), you can set the `Content-Type` header to `text/event-stream`:

```ts highlights={streamHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  })

  const interval = setInterval(() => {
    res.write("data: Streaming data...\n\n")
  }, 3000)

  req.on("close", () => {
    clearInterval(interval)
    res.end()
  })
  
  req.on("end", () => {
    clearInterval(interval)
    res.end()
  })
}
```

The `writeHead` method accepts two parameters:

1. The first parameter is the response's status code.
2. The second parameter is an object of key-value pairs to set the response headers.

This API route opens a stream by setting the `Content-Type` to `text/event-stream`. It then simulates a stream by creating an interval that writes the stream data every three seconds.

#### Tip: Fetching Stream with JS SDK

The JS SDK has a `fetchStream` method that you can use to fetch data from an API route that returns a stream.

Learn more in the [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) documentation.

***

## Do More with Responses

The `MedusaResponse` type is based on [Express's Response](https://expressjs.com/en/api.html#res). Refer to their API reference for other uses of responses.


# Retrieve Custom Links from Medusa's API Route

In this chapter, you'll learn how to retrieve custom data models linked to existing Medusa data models from Medusa's API routes.

## Why Retrieve Custom Linked Data Models?

Often, you'll link custom data models to existing Medusa data models to implement custom features or expand on existing ones.

For example, to add brands for products, you can create a `Brand` data model in a Brand Module, then [define a link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) to the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md)'s `Product` data model.

When you implement this customization, you might need to retrieve the brand of a product using the existing [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid). You can do this by passing the linked data model's name in the `fields` query parameter of the API route.

***

## How to Retrieve Custom Linked Data Models Using `fields`?

Most of Medusa's API routes accept a `fields` query parameter that allows you to specify the fields and relations to retrieve in the resource, such as a product.

For example, to retrieve the brand of a product, you can pass the `brand` field in the `fields` query parameter of the [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid):

```bash
curl 'http://localhost:9000/admin/products/{id}?fields=*brand' \
-H 'Authorization: Bearer {access_token}'
```

The `fields` query parameter accepts a comma-separated list of fields and relations to retrieve. To learn more about using the `fields` query parameter, refer to the [API Reference](https://docs.medusajs.com/api/store#select-fields-and-relations).

By prefixing `brand` with an asterisk (`*`), you retrieve all the default fields of the product, including the `brand` field. If you don't include the `*` prefix, the response will only include the product's brand.

***

## API Routes that Restrict Retrievable Fields

Some of Medusa's API routes restrict the fields and relations you can retrieve, which means you can't pass your custom linked data models in the `fields` query parameter. Medusa makes this restriction to ensure the API routes are performant and secure.

The API routes that restrict the fields and relations you can retrieve are:

- [Customer Store API Routes](https://docs.medusajs.com/api/store#customers)
- [Customer Admin API Routes](https://docs.medusajs.com/api/admin#customers)
- [Product Category Admin API Routes](https://docs.medusajs.com/api/admin#product-categories)

### How to Override Allowed Fields and Relations

For these routes, you need to override the allowed fields and relations to be retrieved. You can do this by applying a [global middleware](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) to those routes.

For example, to allow retrieving the `b2b_company` of a customer using the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid), create the file `src/api/middlewares.ts` with the following content:

Learn how to create a middleware in the [Middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) chapter.

```ts title="src/api/middlewares.ts" highlights={highlights}
import { defineMiddlewares } from "@medusajs/medusa"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/store/customers/me",
      middlewares: [
        (req, res, next) => {
          (req.allowed ??= []).push("b2b_company")
          next()
        },
      ],
    },
  ],
})
```

In this example, you apply a middleware to the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid).

The request object passed to middlewares has an `allowed` property that contains the fields and relations that can be retrieved. So, you modify the `allowed` array to include the `b2b_company` field.

You can now retrieve the `b2b_company` field using the `fields` query parameter of the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid):

```bash
curl 'http://localhost:9000/admin/customers/{id}?fields=*b2b_company' \
-H 'Authorization: Bearer {access_token}'
```

In this example, you retrieve the `b2b_company` relation of the customer using the `fields` query parameter.

This approach only works using a global middleware. It doesn't work in a route middleware.


# Request Body and Query Parameter Validation

In this chapter, you'll learn how to validate request body and query parameters in your custom API route.

## Request Validation

Consider you're creating a `POST` API route at `/custom`. It accepts two parameters `a` and `b` that are required numbers, and returns their sum.

Medusa provides two middlewares to validate the request body and query paramters of incoming requests to your custom API routes:

- `validateAndTransformBody` to validate the request's body parameters against a schema.
- `validateAndTransformQuery` to validate the request's query parameters against a schema.

Both middlewares accept a [Zod](https://zod.dev/) schema as a parameter, which gives you flexibility in how you define your validation schema with complex rules.

The next steps explain how to add request body and query parameter validation to the API route mentioned earlier.

***

## How to Validate Request Body

### Step 1: Create Validation Schema

Medusa uses [Zod](https://zod.dev/) to create validation schemas. These schemas are then used to validate incoming request bodies or query parameters.

To create a validation schema with Zod, create a `validators.ts` file in any `src/api` subfolder. This file holds Zod schemas for each of your API routes.

For example, create the file `src/api/custom/validators.ts` with the following content:

```ts title="src/api/custom/validators.ts"
import { z } from "zod"

export const PostStoreCustomSchema = z.object({
  a: z.number(),
  b: z.number(),
})
```

The `PostStoreCustomSchema` variable is a Zod schema that indicates the request body is valid if:

1. It's an object.
2. It has a property `a` that is a required number.
3. It has a property `b` that is a required number.

### Step 2: Add Request Body Validation Middleware

To use this schema for validating the body parameters of requests to `/custom`, use the `validateAndTransformBody` middleware provided by `@medusajs/framework/http`. It accepts the Zod schema as a parameter.

For example, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { PostStoreCustomSchema } from "./custom/validators"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom",
      method: "POST",
      middlewares: [
        validateAndTransformBody(PostStoreCustomSchema),
      ],
    },
  ],
})
```

This applies the `validateAndTransformBody` middleware on `POST` requests to `/custom`. It uses the `PostStoreCustomSchema` as the validation schema.

#### How the Validation Works

If a request's body parameters don't pass the validation, the `validateAndTransformBody` middleware throws an error indicating the validation errors.

If a request's body parameters are validated successfully, the middleware sets the validated body parameters in the `validatedBody` property of `MedusaRequest`.

### Step 3: Use Validated Body in API Route

In your API route, consume the validated body using the `validatedBody` property of `MedusaRequest`.

For example, create the file `src/api/custom/route.ts` with the following content:

```ts title="src/api/custom/route.ts" highlights={routeHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { z } from "zod"
import { PostStoreCustomSchema } from "./validators"

type PostStoreCustomSchemaType = z.infer<
  typeof PostStoreCustomSchema
>

export const POST = async (
  req: MedusaRequest<PostStoreCustomSchemaType>,
  res: MedusaResponse
) => {
  res.json({
    sum: req.validatedBody.a + req.validatedBody.b,
  })
}
```

In the API route, you use the `validatedBody` property of `MedusaRequest` to access the values of the `a` and `b` properties.

To pass the request body's type as a type parameter to `MedusaRequest`, use Zod's `infer` type that accepts the type of a schema as a parameter.

### Test it Out

To test out the validation, send a `POST` request to `/custom` passing `a` and `b` body parameters. You can try sending incorrect request body parameters to test out the validation.

For example, if you omit the `a` parameter, you'll receive a `400` response code with the following response data:

```json
{
  "type": "invalid_data",
  "message": "Invalid request: Field 'a' is required"
}
```

***

## How to Validate Request Query Parameters

The steps to validate the request query parameters are the similar to that of [validating the body](#how-to-validate-request-body).

### Step 1: Create Validation Schema

The first step is to create a schema with Zod with the rules of the accepted query parameters.

Consider that the API route accepts two query parameters `a` and `b` that are numbers, similar to the previous section.

Create the file `src/api/custom/validators.ts` with the following content:

```ts title="src/api/custom/validators.ts"
import { z } from "zod"

export const PostStoreCustomSchema = z.object({
  a: z.preprocess(
      (val) => {
        if (val && typeof val === "string") {
          return parseInt(val)
        }
        return val
      },
      z
        .number()
    ),
  b: z.preprocess(
    (val) => {
      if (val && typeof val === "string") {
        return parseInt(val)
      }
      return val
    },
    z
      .number()
  ),
})
```

Since a query parameter's type is originally a string or array of strings, you have to use Zod's `preprocess` method to validate other query types, such as numbers.

For both `a` and `b`, you transform the query parameter's value to an integer first if it's a string, then, you check that the resulting value is a number.

### Step 2: Add Request Query Validation Middleware

Next, you'll use the schema to validate incoming requests' query parameters to the `/custom` API route.

Add the `validateAndTransformQuery` middleware to the API route in the file `src/api/middlewares.ts`:

```ts title="src/api/middlewares.ts"
import { 
  validateAndTransformQuery,
  defineMiddlewares,
} from "@medusajs/framework/http"
import { PostStoreCustomSchema } from "./custom/validators"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom",
      method: "POST",
      middlewares: [
        validateAndTransformQuery(
          PostStoreCustomSchema,
          {}
        ),
      ],
    },
  ],
})
```

The `validateAndTransformQuery` accepts two parameters:

- The first one is the Zod schema to validate the query parameters against.
- The second one is an object of options for retrieving data using Query, which you can learn more about in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md).

#### How the Validation Works

If a request's query parameters don't pass the validation, the `validateAndTransformQuery` middleware throws an error indicating the validation errors.

If a request's query parameters are validated successfully, the middleware sets the validated query parameters in the `validatedQuery` property of `MedusaRequest`.

### Step 3: Use Validated Query in API Route

Finally, use the validated query in the API route. The `MedusaRequest` parameter has a `validatedQuery` parameter that you can use to access the validated parameters.

For example, create the file `src/api/custom/route.ts` with the following content:

```ts title="src/api/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const a = req.validatedQuery.a as number
  const b = req.validatedQuery.b as number

  res.json({
    sum: a + b,
  })
}
```

In the API route, you use the `validatedQuery` property of `MedusaRequest` to access the values of the `a` and `b` properties as numbers, then return in the response their sum.

### Test it Out

To test out the validation, send a `POST` request to `/custom` with `a` and `b` query parameters. You can try sending incorrect query parameters to see how the validation works.

For example, if you omit the `a` parameter, you'll receive a `400` response code with the following response data:

```json
{
  "type": "invalid_data",
  "message": "Invalid request: Field 'a' is required"
}
```

***

## Learn More About Validation Schemas

To see different examples and learn more about creating a validation schema, refer to [Zod's documentation](https://zod.dev).


# Custom CLI Scripts

In this chapter, you'll learn how to create and execute custom scripts using Medusa's CLI tool.

## What is a Custom CLI Script?

A custom CLI script is a function that you can execute using Medusa's CLI tool. It is useful when you need a script that has access to the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) and can be executed using Medusa's CLI.

For example, you can create a custom CLI script that:

- [Seeds data into the database](https://docs.medusajs.com/learn/fundamentals/custom-cli-scripts/seed-data/index.html.md).
- Runs a script before starting the Medusa application.

***

## How to Create a Custom CLI Script?

To create a custom CLI script, create a TypeScript or JavaScript file under the `src/scripts` directory. The file must export a function by default.

For example, create the file `src/scripts/my-script.ts` with the following content:

```ts title="src/scripts/my-script.ts"
import { 
  ExecArgs,
} from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"

export default async function myScript({ container }: ExecArgs) {
  const productModuleService = container.resolve(
    Modules.PRODUCT
  )

  const [, count] = await productModuleService
    .listAndCountProducts()

  console.log(`You have ${count} product(s)`)
}
```

The function receives as a parameter an object with a `container` property, which is an instance of the Medusa Container. Use it to resolve resources in your Medusa application.

***

## How to Run a Custom CLI Script?

To run a custom CLI script, run the Medusa CLI's `exec` command:

```bash
npx medusa exec ./src/scripts/my-script.ts
```

***

## Custom CLI Script Arguments

Your script can accept arguments from the command line. Arguments are passed to the function's object parameter in the `args` property.

For example, create the following CLI script that logs the command line arguments:

```ts
import { ExecArgs } from "@medusajs/framework/types"

export default async function myScript({ args }: ExecArgs) {
  console.log(`The arguments you passed: ${args}`)
}
```

Then, run the script with the `exec` command and pass arguments after the script's path.

```bash
npx medusa exec ./src/scripts/my-script.ts arg1 arg2
```

***

## Run Custom Script on Application Startup

In some cases, you may need to perform an action when the Medusa application starts.

If the action is related to a module, you should use a [loader](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md). Otherwise, you can create a custom CLI script and run it before starting the Medusa application.

To run a custom script on application startup, modify the `dev` and `start` commands in `package.json` to execute your script first.

For example:

```json title="package.json"
{
  "scripts": {
    "startup": "medusa exec ./src/scripts/startup.ts",
    "dev": "npm run startup && medusa develop",
    "start": "npm run startup && medusa start"
  }
}
```

The `startup` script will run every time you run the Medusa application.


# Seed Data with Custom CLI Script

In this chapter, you'll learn how to seed data using a [custom CLI script](https://docs.medusajs.com/learn/fundamentals/custom-cli-scripts/index.html.md).

## How to Seed Data

To seed dummy data for development or demo purposes, use a custom CLI script.

In the CLI script, use your custom workflows or Medusa's existing workflows, which you can browse in [this reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md), to seed data.

***

## Example: Seed Dummy Products

In this section, you'll follow an example of creating a custom CLI script that seeds fifty dummy products.

First, install the [Faker](https://fakerjs.dev/) library to generate random data in your script:

```bash npm2yarn
npm install --save-dev @faker-js/faker
```

Then, create the file `src/scripts/demo-products.ts` with the following content:

```ts title="src/scripts/demo-products.ts" highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports"
import { ExecArgs } from "@medusajs/framework/types"
import { faker } from "@faker-js/faker"
import { 
  ContainerRegistrationKeys, 
  Modules, 
  ProductStatus,
} from "@medusajs/framework/utils"
import { 
  createInventoryLevelsWorkflow,
  createProductsWorkflow,
} from "@medusajs/medusa/core-flows"

export default async function seedDummyProducts({
  container,
}: ExecArgs) {
  const salesChannelModuleService = container.resolve(
    Modules.SALES_CHANNEL
  )
  const logger = container.resolve(
    ContainerRegistrationKeys.LOGGER
  )
  const query = container.resolve(
    ContainerRegistrationKeys.QUERY
  )

  const defaultSalesChannel = await salesChannelModuleService
    .listSalesChannels({
      name: "Default Sales Channel",
    })

  const sizeOptions = ["S", "M", "L", "XL"]
  const colorOptions = ["Black", "White"]
  const currency_code = "eur"
  const productsNum = 50

  // TODO seed products
}
```

So far, in the script, you:

- Resolve the [Sales Channel Module](https://docs.medusajs.com/resources/commerce-modules/sales-channel/index.html.md)'s service to retrieve the application's default sales channel. This is the sales channel the dummy products will be available in.
- Resolve the [Logger](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) to log messages in the terminal, and Query to later retrieve data useful for the seeded products.
- Initialize some default data to use when seeding the products next.

Next, replace the `TODO` with the following:

```ts title="src/scripts/demo-products.ts"
const productsData = new Array(productsNum).fill(0).map((_, index) => {
  const title = faker.commerce.product() + "_" + index
  return {
    title,
    is_giftcard: false,
    description: faker.commerce.productDescription(),
    status: ProductStatus.PUBLISHED,
    options: [
      {
        title: "Size",
        values: sizeOptions,
      },
      {
        title: "Color",
        values: colorOptions,
      },
    ],
    images: [
      {
        url: faker.image.urlPlaceholder({
          text: title,
        }),
      },
      {
        url: faker.image.urlPlaceholder({
          text: title,
        }),
      },
    ],
    variants: new Array(10).fill(0).map((_, variantIndex) => ({
      title: `${title} ${variantIndex}`,
      sku: `variant-${variantIndex}${index}`,
      prices: new Array(10).fill(0).map((_, priceIndex) => ({
        currency_code,
        amount: 10 * priceIndex,
      })),
      options: {
        Size: sizeOptions[Math.floor(Math.random() * 3)],
      },
    })),
    shipping_profile_id: "sp_123",
    sales_channels: [
      {
        id: defaultSalesChannel[0].id,
      },
    ],
  }
})

// TODO seed products
```

You generate fifty products using the sales channel and variables you initialized, and using Faker for random data, such as the product's title or images.

Then, replace the new `TODO` with the following:

```ts title="src/scripts/demo-products.ts"
const { result: products } = await createProductsWorkflow(container).run({
  input: {
    products: productsData,
  },
})

logger.info(`Seeded ${products.length} products.`)

// TODO add inventory levels
```

You create the generated products using the `createProductsWorkflow` imported previously from `@medusajs/medusa/core-flows`. It accepts the product data as input, and returns the created products.

The only thing left is to create [inventory levels](https://docs.medusajs.com/resources/commerce-modules/inventory/concepts#inventorylevel/index.html.md) for the product variants. So, replace the last `TODO` with the following:

```ts title="src/scripts/demo-products.ts"
logger.info("Seeding inventory levels.")

const { data: stockLocations } = await query.graph({
  entity: "stock_location",
  fields: ["id"],
})

const { data: inventoryItems } = await query.graph({
  entity: "inventory_item",
  fields: ["id"],
})

const inventoryLevels = inventoryItems.map((inventoryItem) => ({
  location_id: stockLocations[0].id,
  stocked_quantity: 1000000,
  inventory_item_id: inventoryItem.id,
}))

await createInventoryLevelsWorkflow(container).run({
  input: {
    inventory_levels: inventoryLevels,
  },
})

logger.info("Finished seeding inventory levels data.")
```

You use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) to retrieve the first stock location in the application and the inventory items.

Then, you generate inventory levels for each inventory item, associating it with the first stock location.

Finally, you use the `createInventoryLevelsWorkflow` from Medusa's core workflows to create the inventory levels.

### Test Script

To test out the script, run the following command in your project's directory:

```bash
npx medusa exec ./src/scripts/demo-products.ts
```

This seeds the products to your database. If you run your Medusa application and view the products in the dashboard, you'll find fifty new products.


# Add Data Model Check Constraints

In this chapter, you'll learn how to add check constraints to your data model.

## What is a Check Constraint?

A check constraint is a condition that must be satisfied by records inserted into a database table, otherwise an error is thrown.

For example, if you have a data model with a `price` property, you want to only allow positive number values. So, you add a check constraint that fails when inserting a record with a negative price value.

***

## How to Set a Check Constraint?

To set check constraints on a data model, use the `checks` method. This method accepts an array of check constraints to apply on the data model.

For example, to set a check constraint on a `price` property that ensures its value can only be a positive number:

```ts highlights={checks1Highlights}
import { model } from "@medusajs/framework/utils"

const CustomProduct = model.define("custom_product", {
  // ...
  price: model.bigNumber(),
})
.checks([
  (columns) => `${columns.price} >= 0`,
])
```

The item passed in the array parameter of `checks` can be a callback function that accepts as a parameter an object whose keys are the names of the properties in the data model schema, and values the respective column name in the database.

The function returns a string indicating the [SQL check constraint expression](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS). In the expression, use the `columns` parameter to access a property's column name.

You can also pass an object to the `checks` method:

```ts highlights={checks2Highlights}
import { model } from "@medusajs/framework/utils"

const CustomProduct = model.define("custom_product", {
  // ...
  price: model.bigNumber(),
})
.checks([
  {
    name: "custom_product_price_check",
    expression: (columns) => `${columns.price} >= 0`,
  },
])
```

The object accepts the following properties:

- `name`: The check constraint's name.
- `expression`: A function similar to the one that can be passed to the array. It accepts an object of columns and returns an [SQL check constraint expression](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS).

***

## Apply in Migrations

After adding the check constraint, make sure to generate and run migrations if you already have the table in the database. Otherwise, the check constraint won't be reflected.

To generate a migration for the data model's module then reflect it on the database, run the following command:

```bash
npx medusa db:generate custom_module
npx medusa db:migrate
```

The first command generates the migration under the `migrations` directory of your module's directory, and the second reflects it on the database.

***

## Examples

This section covers common use cases where check constraints are particularly useful.

### 1. Enforce Text Length

Ensure that text properties meet minimum or maximum length requirements:

```ts
const User = model.define("user", {
  username: model.text(),
  password: model.text(),
})
.checks([
  {
    name: "password_length_check", 
    expression: (columns) => `LENGTH(${columns.password}) >= 8`,
  },
])
```

In the above example, the check constraint fails if the `password` property is less than 8 characters long.

### 2. Validate Email Format

Ensure email addresses contain the `@` symbol:

```ts
const Customer = model.define("customer", {
  email: model.text(),
})
.checks([
  {
    name: "email_format_check",
    expression: (columns) => `${columns.email} LIKE '%@%'`,
  },
])
```

In the above example, the check constraint fails if the `email` property does not contain the `@` symbol.

### 3. Enforce Date Ranges

Ensure dates fall within valid ranges:

```ts
const Event = model.define("event", {
  start_date: model.dateTime(),
  end_date: model.dateTime(),
})
.checks([
  {
    name: "date_order_check",
    expression: (columns) => `${columns.end_date} >= ${columns.start_date}`,
  },
])
```

In the above example, the check constraint fails if the `end_date` is earlier than the `start_date`.


# Data Model Database Index

In this chapter, you’ll learn how to define a database index on a data model.

You can also define an index on a property as explained in the [Properties chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties#define-database-index-on-property/index.html.md).

## Define Database Index on Data Model

A data model has an `indexes` method that defines database indices on its properties.

The index can be on multiple columns (composite index). For example:

```ts highlights={dataModelIndexHighlights}
import { model } from "@medusajs/framework/utils"

const MyCustom = model.define("my_custom", {
  id: model.id().primaryKey(),
  name: model.text(),
  age: model.number(),
}).indexes([
  {
    on: ["name", "age"],
  },
])

export default MyCustom
```

The `indexes` method receives an array of indices as a parameter. Each index is an object with a required `on` property indicating the properties to apply the index on.

In the above example, you define a composite index on the `name` and `age` properties.

### Index Conditions

An index can have conditions. For example:

```ts highlights={conditionHighlights}
import { model } from "@medusajs/framework/utils"

const MyCustom = model.define("my_custom", {
  id: model.id().primaryKey(),
  name: model.text(),
  age: model.number(),
}).indexes([
  {
    on: ["name", "age"],
    where: {
      age: 30,
    },
  },
])

export default MyCustom
```

The index object passed to `indexes` accepts a `where` property whose value is an object of conditions. The object's key is a property's name, and its value is the condition on that property.

In the example above, the composite index is created on the `name` and `age` properties when the `age`'s value is `30`.

A property's condition can be a negation. For example:

```ts highlights={negationHighlights}
import { model } from "@medusajs/framework/utils"

const MyCustom = model.define("my_custom", {
  id: model.id().primaryKey(),
  name: model.text(),
  age: model.number().nullable(),
}).indexes([
  {
    on: ["name", "age"],
    where: {
      age: {
        $ne: null,
      },
    },
  },
])

export default MyCustom
```

A property's value in `where` can be an object having a `$ne` property. `$ne`'s value indicates what the specified property's value shouldn't be.

In the example above, the composite index is created on the `name` and `age` properties when `age`'s value is not `null`.

### Unique Database Index

The object passed to `indexes` accepts a `unique` property indicating that the created index must be a unique index.

For example:

```ts highlights={uniqueHighlights}
import { model } from "@medusajs/framework/utils"

const MyCustom = model.define("my_custom", {
  id: model.id().primaryKey(),
  name: model.text(),
  age: model.number(),
}).indexes([
  {
    on: ["name", "age"],
    unique: true,
  },
])

export default MyCustom
```

This creates a unique composite index on the `name` and `age` properties.


# Infer Type of Data Model

In this chapter, you'll learn how to infer the type of a data model.

## How to Infer Type of Data Model?

Consider you have a `Post` data model. You can't reference this data model in a type, such as a workflow input or service method output types, since it's a variable.

Instead, Medusa provides `InferTypeOf` that transforms your data model to a type.

For example:

```ts
import { InferTypeOf } from "@medusajs/framework/types"
import { Post } from "../modules/blog/models/post" // relative path to the model

export type Post = InferTypeOf<typeof Post>
```

`InferTypeOf` accepts as a type argument the type of the data model.

Since the `Post` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`.

You can now use the `Post` type to reference a data model in other types, such as in workflow inputs or service method outputs:

```ts title="Example Service"
// other imports...
import { InferTypeOf } from "@medusajs/framework/types"
import { Post } from "../models/post"

type Post = InferTypeOf<typeof Post>

class BlogModuleService extends MedusaService({ Post }) {
  async doSomething(): Promise<Post> {
    // ...
  }
}
```


# JSON Properties in Data Models

In this chapter, you'll learn how to use and manage [JSON properties](https://docs.medusajs.com/learn/fundamentals/data-models/properties#json/index.html.md) in data models.

## What is a JSON Property?

A JSON property in a data model is a flexible object that can contain various types of values.

For example, you can create a `Brand` data model with a `metadata` JSON property to store additional information about the brand:

```ts highlights={[["6"]]}
import { model } from "@medusajs/framework/utils"

const Brand = model.define("brand", {
  id: model.id().primaryKey(),
  name: model.text(),
  metadata: model.json(),
})
```

### Accepted Values in JSON Property

JSON properties are made up of key-value pairs. The keys are strings, and the values can be one of the following types:

- **Strings**: Text values.
  - Empty strings remove the property from the JSON object. Learn more in the [Remove a Property from the JSON Property](#remove-a-property-from-the-json-property) section.
- **Numbers**: Numeric values.
- **Booleans**: `true` or `false` values.
- **Nested Objects**: Objects within objects.
- **Arrays**: Lists of values of any of the above types.

For example, a `metadata` JSON property can look like this:

```json
{
  "category": "electronics",
  "views": 1500,
  "is_featured": true,
  "tags": ["new", "sale"],
  "details": {
    "warranty": "2 years",
    "origin": "USA"
  }
}
```

### What are JSON Properties Useful For?

JSON properties allow you to store flexible and dynamic data structures that can evolve over time without requiring changes to the database schema.

Most data models in Medusa's Commerce Modules have a `metadata` property that is a JSON object. `metadata` allows you to store custom information that is not part of the core data model.

Some examples of data to store in JSON properties:

- Custom gift message for line items in an order.
- Product's ID in a third-party system.
- Brand's category or tags.

### What are JSON Properties Not Useful For?

JSON properties are not suitable for structured data that requires strict validation or relationships with other data models.

For example, if you want to re-use brands across different products, it's better to create a `Brand` data model and [define a link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) to the `Product` data model instead of storing brand information in the product's `metadata` JSON property.

***

## How to Manage JSON Properties?

### How Medusa Updates JSON Properties

Consider a Brand Module with a `Brand` data model as shown [in the previous section](#what-is-a-json-property). The [module's service](https://docs.medusajs.com/learn/fundamentals/modules#2-create-service/index.html.md) will extend `MedusaService`, which generates methods like [updateBrands](https://docs.medusajs.com/resources/service-factory-reference/methods/update/index.html.md) to update a brand.

When you pass a JSON property in the `updateBrands` method, Medusa will merge the provided JSON object with the existing one in the database. So, only the properties you pass will be updated, and the rest will remain unchanged.

The following sections show examples of how to add, update, and remove properties in a JSON property.

The following examples assume you have a `brandModuleService` that is resolved from the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md). For example:

```ts
const brandModuleService = container.resolve(BRAND_MODULE)
```

### Add a Property to the JSON Property

Continuing with the Brand example, to add a `category` property to the `metadata` property, pass the new property in the `update` or `create` methods:

```ts
const brand = await brandModuleService.updateBrands({
  id: "brand_123",
  metadata: {
    category: "electronics",
  },
})
```

The brand record will now have the `metadata` property updated to include the new `category` property:

```json title="Result"
{
  "id": "brand_123",
  "metadata": {
    "category": "electronics"
  }
}
```

If you want to add another `is_featured` property later, you can do so by passing it in the update method again without affecting the existing properties:

```ts
const brand = await brandModuleService.updateBrands({
  id: "brand_123",
  metadata: {
    is_featured: true,
  },
})
```

The brand record will now have the `metadata` property updated to include both `category` and `is_featured` properties:

```json title="Result"
{
  "id": "brand_123",
  "metadata": {
    "category": "electronics",
    "is_featured": true
  }
}
```

### Update an Existing Property in the JSON Property

To update an existing property in the JSON property, pass the updated value in the `update` method.

Continuing with the Brand example, to update the `category` property in the `metadata`:

```ts
const brand = await brandModuleService.updateBrands({
  id: "brand_123",
  metadata: {
    category: "home appliances",
  },
})
```

The brand record will now have the `metadata` property updated to reflect the new `category` value, and existing properties will remain unchanged:

```json title="Result"
{
  "id": "brand_123",
  "metadata": {
    "category": "home appliances",
    "is_featured": true
  }
}
```

### Caveat: Updating Nested Objects

If you want to update a nested object within the JSON property, you need to provide the entire nested object in the update method.

For example, consider that you have a `details` object within the `metadata`:

```json
{
  "id": "brand_123",
  "metadata": {
    "category": "electronics",
    "details": {
      "warranty": "1 year",
      "origin": "China"
    }
  }
}
```

To update the `warranty` property within the `details` object, you need to provide the entire `details` object in the update method:

```ts
const brand = await brandModuleService.updateBrands({
  id: "brand_123",
  metadata: {
    details: {
      warranty: "2 years",
      origin: "China",
    },
  },
})
```

The brand record will now have the `metadata` property updated with the new `warranty` value:

```json title="Result"
{
  "id": "brand_123",
  "metadata": {
    "category": "electronics",
    "details": {
      "warranty": "2 years",
      "origin": "China"
    }
  }
}
```

### Remove a Property from the JSON Property

To remove a property from the JSON property, you can pass an empty string as the value of the property in the update method.

For example, to remove the `is_featured` property from the `metadata`:

```ts
const brand = await brandModuleService.updateBrands({
  id: "brand_123",
  metadata: {
    is_featured: "",
  },
})
```

The brand record will now have the `metadata` property updated to remove the `is_featured` property:

```json title="Result"
{
  "id": "brand_123",
  "metadata": {
    "category": "home appliances"
  }
}
```

### Manage JSON Properties in API Routes

The instructions above also apply when managing JSON properties in API routes, since they typically use a service's `update` method under the hood.

Refer to the [API reference](https://docs.medusajs.com/api/store#manage-metadata) to learn more about managing JSON properties, such as `metadata`, in API routes.


# Manage Relationships

In this chapter, you'll learn how to manage relationships between data models when creating, updating, or retrieving records using the module's main service.

This chapter applies to data model relationships within the same module. To manage linked data models across modules, check out [Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) and [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md).

## Manage One-to-One Relationship

### BelongsTo Side of One-to-One

When you create a record of a data model that belongs to another through a one-to-one relation, pass the ID of the other data model's record in the `{relation}_id` property, where `{relation}` is the name of the relation property.

For example, assuming you have the [User and Email data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md), set an email's user ID as follows:

```ts highlights={belongsHighlights}
// when creating an email
const email = await helloModuleService.createEmails({
  // other properties...
  user_id: "123",
})

// when updating an email
const email = await helloModuleService.updateEmails({
  id: "321",
  // other properties...
  user_id: "123",
})
```

In the example above, you pass the `user_id` property when creating or updating an email to specify the user it belongs to.

### HasOne Side

When you create a record of a data model that has one of another, pass the ID of the other data model's record in the relation property.

For example, assuming you have the [User and Email data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md), set a user's email ID as follows:

```ts highlights={hasOneHighlights}
// when creating a user
const user = await helloModuleService.createUsers({
  // other properties...
  email: "123",
})

// when updating a user
const user = await helloModuleService.updateUsers({
  id: "321",
  // other properties...
  email: "123",
})
```

In the example above, you pass the `email` property when creating or updating a user to specify the email it has.

***

## Manage One-to-Many Relationship

In a one-to-many relationship, you can only manage the associations from the `belongsTo` side.

When you create a record of the data model on the `belongsTo` side, pass the ID of the other data model's record in the `{relation}_id` property, where `{relation}` is the name of the relation property.

For example, assuming you have the [Product and Store data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-many-relationship/index.html.md), set a product's store ID as follows:

```ts highlights={manyBelongsHighlights}
// when creating a product
const product = await helloModuleService.createProducts({
  // other properties...
  store_id: "123",
})

// when updating a product
const product = await helloModuleService.updateProducts({
  id: "321",
  // other properties...
  store_id: "123",
})
```

In the example above, you pass the `store_id` property when creating or updating a product to specify the store it belongs to.

***

## Manage Many-to-Many Relationship

If your many-to-many relation is represented with a [pivotEntity](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), refer to [this section](#manage-many-to-many-relationship-with-pivotentity) instead.

### Create Associations

When you create a record of a data model that has a many-to-many relationship to another data model, pass an array of IDs of the other data model's records in the relation property.

For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), set the association between products and orders as follows:

```ts highlights={manyHighlights}
// when creating a product
const product = await helloModuleService.createProducts({
  // other properties...
  orders: ["123", "321"],
})

// when creating an order
const order = await helloModuleService.createOrders({
  id: "321",
  // other properties...
  products: ["123", "321"],
})
```

In the example above, you pass the `orders` property when you create a product, and you pass the `products` property when you create an order.

### Update Associations

When you use the `update` methods generated by the service factory, you also pass an array of IDs as the relation property's value to add new associated records.

However, this removes any existing associations to records whose IDs aren't included in the array.

For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), you update the product's related orders as so:

```ts
const product = await helloModuleService.updateProducts({
  id: "123",
  // other properties...
  orders: ["321"],
})
```

If the product was associated with an order, and you don't include that order's ID in the `orders` array, the association between the product and order is removed.

So, to add a new association without removing existing ones, retrieve the product first to pass its associated orders when updating the product:

```ts highlights={updateAssociationHighlights}
const product = await helloModuleService.retrieveProduct(
  "123",
  {
    relations: ["orders"],
  }
)

const updatedProduct = await helloModuleService.updateProducts({
  id: product.id,
  // other properties...
  orders: [
    ...product.orders.map((order) => order.id),
    "321",
  ],
})
```

This keeps existing associations between the product and orders, and adds a new one.

***

## Manage Many-to-Many Relationship with pivotEntity

If your many-to-many relation is represented without a [pivotEntity](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), refer to [this section](#manage-many-to-many-relationship) instead.

If you have a many-to-many relation with a `pivotEntity` specified, make sure to pass the data model representing the pivot table to [MedusaService](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) that your module's service extends.

For example, assuming you have the [Order, Product, and OrderProduct models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), add `OrderProduct` to `MedusaService`'s object parameter:

```ts highlights={["4"]}
class BlogModuleService extends MedusaService({
  Order,
  Product,
  OrderProduct,
}) {}
```

This will generate Create, Read, Update and Delete (CRUD) methods for the `OrderProduct` data model, which you can use to create relations between orders and products and manage the extra columns in the pivot table.

For example:

```ts
// create order-product association
const orderProduct = await blogModuleService.createOrderProducts({
  order_id: "123",
  product_id: "123",
  metadata: {
    test: true,
  },
})

// update order-product association
const orderProduct = await blogModuleService.updateOrderProducts({
  id: "123",
  metadata: {
    test: false,
  },
})

// delete order-product association
await blogModuleService.deleteOrderProducts("123")
```

Since the `OrderProduct` data model belongs to the `Order` and `Product` data models, you can set its order and product as explained in the [one-to-many relationship section](#manage-one-to-many-relationship) using `order_id` and `product_id`.

Refer to the [service factory reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for a full list of generated methods and their usages.

***

## Retrieve Records of Relation

The `list`, `listAndCount`, and `retrieve` methods of a module's main service accept as a second parameter an object of options.

To retrieve the records associated with a data model's records through a relationship, pass in the second parameter object a `relations` property whose value is an array of relationship names.

For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), you retrieve a product's orders as follows:

```ts highlights={retrieveHighlights}
const product = await blogModuleService.retrieveProducts(
  "123",
  {
    relations: ["orders"],
  }
)
```

In the example above, the retrieved product has an `orders` property, whose value is an array of orders associated with the product.


# Data Models

In this chapter, you'll learn what a data model is and how to create a data model.

## What is a Data Model?

A data model represents a table in the database. You create data models using Medusa's data modeling language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

You create a data model in a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). The module's service provides the methods to store and manage those data models. Then, you can resolve the module's service in other customizations, such as a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), to manage the data models' records.

***

## How to Create a Data Model

In a module, you can create a data model in a TypeScript or JavaScript file under the module's `models` directory.

So, for example, assuming you have a Blog Module at `src/modules/blog`, you can create a `Post` data model by creating the `src/modules/blog/models/post.ts` file with the following content:

![Updated directory overview after adding the data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732806790/Medusa%20Book/blog-dir-overview-1_jfvovj.jpg)

```ts title="src/modules/blog/models/post.ts"
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id().primaryKey(),
  title: model.text(),
})

export default Post
```

You define the data model using the `define` method of the DML. It accepts two parameters:

1. The first one is the name of the data model's table in the database. Use snake-case names.
2. The second is an object, which is the data model's schema. The schema's properties are defined using the `model`'s methods, such as `text` and `id`.
   - Data models automatically have the date properties `created_at`, `updated_at`, and `deleted_at`, so you don't need to add them manually.

The code snippet above defines a `Post` data model with `id` and `title` properties.

***

## Generate Migrations

After you create a data model in a module, then [register that module in your Medusa configurations](https://docs.medusajs.com/learn/fundamentals/modules#4-add-module-to-medusas-configurations/index.html.md), you must generate a migration to create the data model's table in the database.

A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations.

For example, to generate a migration for the Blog Module, run the following command in your Medusa application's directory:

If you're creating the module in a plugin, use the [plugin:db:generate command](https://docs.medusajs.com/resources/medusa-cli/commands/plugin#plugindbgenerate/index.html.md) instead.

```bash
npx medusa db:generate blog
```

The `db:generate` command of the Medusa CLI accepts one or more module names to generate the migration for. It will create a migration file for the Blog Module in the directory `src/modules/blog/migrations` similar to the following:

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 "@medusajs/framework/mikro-orm/migrations"

export class Migration20241121103722 extends Migration {

  async up(): Promise<void> {
    this.addSql("create table if not exists \"post\" (\"id\" text not null, \"title\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"post_pkey\" primary key (\"id\"));")
  }

  async down(): Promise<void> {
    this.addSql("drop table if exists \"post\" cascade;")
  }

}
```

In the migration class, the `up` method creates the table `post` and defines its columns using PostgreSQL syntax. The `down` method drops the table.

### Run Migrations

To reflect the changes in the generated migration file on the database, run the `db:migrate` command:

If you're creating the module in a plugin, run this command on the Medusa application that the plugin is installed in.

```bash
npx medusa db:migrate
```

This creates the `post` table in the database.

### Migrations on Data Model Changes

Whenever you make a change to a data model, you must generate and run the migrations.

For example, if you add a new column to the `Post` data model, you must generate a new migration and run it.

***

## Manage Data Models

Your module's service should extend the [service factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md), which generates data-management methods for your module's data models.

For example, the Blog Module's service would have methods like `retrievePost` and `createPosts`.

Refer to the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) chapter to learn more about how to extend the service factory and manage data models, and refer to the [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for the full list of generated methods and how to use them.


# Data Model Properties

In this chapter, you'll learn about the different property types you can use in a data model and how to configure a data model's properties.

## Data Model's Default Properties

By default, Medusa creates the following properties for every data model:

- `created_at`: A [dateTime](#dateTime) property that stores when a record of the data model was created.
- `updated_at`: A [dateTime](#dateTime) property that stores when a record of the data model was updated.
- `deleted_at`: A [dateTime](#dateTime) property that stores when a record of the data model was deleted. When you soft-delete a record, Medusa sets the `deleted_at` property to the current date.

***

## Property Types

This section covers the different property types you can define in a data model's schema using the `model` methods.

### id

The `id` method defines an automatically generated string ID property. The generated ID is a unique string that has a mix of letters and numbers.

For example:

```ts highlights={idHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id(),
  // ...
})

export default Post
```

### text

The `text` method defines a string property.

For example:

```ts highlights={textHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  name: model.text(),
  // ...
})

export default Post
```

#### Limit Text Length

To limit the allowed length of a `text` property, use the [checks method](https://docs.medusajs.com/learn/fundamentals/data-models/check-constraints/index.html.md).

For example, to limit the `name` property to a maximum of 50 characters:

```ts highlights={textLengthHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  name: model.text(),
  // ...
})
.checks([
  {
    name: "limit_name_length",
    expression: (columns) => `LENGTH(${columns.name}) <= 50`,
  },
])

export default Post
```

This will add a database check constraint that ensures the `name` property of a record does not exceed 50 characters. If a record with a longer `name` is attempted to be inserted, an error will be thrown.

### number

The `number` method defines a number property.

For example:

```ts highlights={numberHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  age: model.number(),
  // ...
})

export default Post
```

### float

This property is only available after [Medusa v2.1.2](https://github.com/medusajs/medusa/releases/tag/v2.1.2).

The `float` method defines a number property that allows for values with decimal places.

Use this property type when it's less important to have high precision for numbers with large decimal places. Alternatively, for higher precision, use the [bigNumber property](#bignumber).

For example:

```ts highlights={floatHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  rating: model.float(),
  // ...
})

export default Post
```

### bigNumber

The `bigNumber` method defines a number property that expects large numbers, such as prices.

Use this property type when it's important to have high precision for numbers with large decimal places. Alternatively, for less percision, use the [float property](#float).

For example:

```ts highlights={bigNumberHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  price: model.bigNumber(),
  // ...
})

export default Post
```

### boolean

The `boolean` method defines a boolean property.

For example:

```ts highlights={booleanHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  hasAccount: model.boolean(),
  // ...
})

export default Post
```

### enum

The `enum` method defines a property whose value can only be one of the specified values.

For example:

```ts highlights={enumHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  color: model.enum(["black", "white"]),
  // ...
})

export default Post
```

The `enum` method accepts an array of possible string values.

### dateTime

The `dateTime` method defines a timestamp property.

For example:

```ts highlights={dateTimeHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  date_of_birth: model.dateTime(),
  // ...
})

export default Post
```

### json

The `json` method defines a property whose value is stored as a stringified JSON object in the database.

For example:

```ts highlights={jsonHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  metadata: model.json(),
  // ...
})

export default Post
```

Learn more in the [JSON Properties](https://docs.medusajs.com/learn/fundamentals/data-models/json-properties/index.html.md) chapter.

### array

The `array` method defines an array of strings property.

For example:

```ts highlights={arrHightlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  names: model.array(),
  // ...
})

export default Post
```

### Properties Reference

Refer to the [Data Model Language (DML) reference](https://docs.medusajs.com/resources/references/data-model/index.html.md) for a full reference of the properties.

***

## Set Primary Key Property

To set any `id`, `text`, or `number` property as a primary key, use the `primaryKey` method.

For example:

```ts highlights={highlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id().primaryKey(),
  // ...
})

export default Post
```

In the example above, the `id` property is defined as the data model's primary key.

***

## Property Default Value

Use the `default` method on a property's definition to specify the default value of a property.

For example:

```ts highlights={defaultHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  color: model
    .enum(["black", "white"])
    .default("black"),
  age: model
    .number()
    .default(0),
  // ...
})

export default Post
```

In this example, you set the default value of the `color` enum property to `black`, and that of the `age` number property to `0`.

***

## Make Property Optional

Use the `nullable` method to indicate that a property’s value can be `null`. This is useful when you want a property to be optional.

For example:

```ts highlights={nullableHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  price: model.bigNumber().nullable(),
  // ...
})

export default Post
```

In the example above, the `price` property is configured to allow `null` values, making it optional.

***

## Unique Property

The `unique` method indicates that a property’s value must be unique in the database through a unique index.

For example:

```ts highlights={uniqueHighlights}
import { model } from "@medusajs/framework/utils"

const User = model.define("user", {
  email: model.text().unique(),
  // ...
})

export default User
```

In this example, multiple users can’t have the same email.

***

## Define Database Index on Property

Use the `index` method on a property's definition to define a database index.

For example:

```ts highlights={dbIndexHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id().primaryKey(),
  name: model.text().index(
    "IDX_MY_CUSTOM_NAME"
  ),
})

export default Post
```

The `index` method optionally accepts the name of the index as a parameter.

In this example, you define an index on the `name` property.

***

## Define a Searchable Property

Methods generated by the [service factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) that accept filters, such as `list{ModelName}s`, accept a `q` property as part of the filters.

When the `q` filter is passed, the data model's searchable properties are queried to find matching records.

Use the `searchable` method on a `text` property to indicate that it's searchable.

For example:

```ts highlights={searchableHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  title: model.text().searchable(),
  // ...
})

export default Post
```

In this example, the `title` property is searchable.

### Search Example

If you pass a `q` filter to the `listPosts` method:

```ts
const posts = await blogModuleService.listPosts({
  q: "New Products",
})
```

This retrieves records that include `New Products` in their `title` property.


# Data Model Relationships

In this chapter, you’ll learn how to define relationships between data models in your module.

## What is a Relationship Property?

A relationship property defines an association in the database between two models. It's created using the Data Model Language (DML) methods, such as `hasOne` or `belongsTo`.

When you generate a migration for these data models, the migrations include foreign key columns or pivot tables, based on the relationship's type.

You want to create a relation between data models in the same module.

You want to create a relationship between data models in different modules. Use module links instead.

***

## One-to-One Relationship

A one-to-one relationship indicates that one record of a data model belongs to or is associated with another.

To define a one-to-one relationship, create relationship properties in the data models using the following methods:

1. `hasOne`: indicates that the model has one record of the specified model.
2. `belongsTo`: indicates that the model belongs to one record of the specified model.

For example:

```ts highlights={oneToOneHighlights}
import { model } from "@medusajs/framework/utils"

const User = model.define("user", {
  id: model.id().primaryKey(),
  email: model.hasOne(() => Email, {
    mappedBy: "user",
  }),
})

const Email = model.define("email", {
  id: model.id().primaryKey(),
  user: model.belongsTo(() => User, {
    mappedBy: "email",
  }),
})
```

In the example above, a user has one email, and an email belongs to one user.

The `hasOne` and `belongsTo` methods accept a function as the first parameter. The function returns the associated data model.

Both methods also accept a second parameter object with the property `mappedBy`. Its value is the name of the relationship property in the other data model.

### Optional Relationship

To make the relationship optional on the `hasOne` or `belongsTo` side, use the `nullable` method on either property as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties#make-property-optional/index.html.md).

### One-to-One Relationship in the Database

When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the `belongsTo` property:

1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `email` table will have a `user_id` column.
2. A foreign key on the `{relation_name}_id` column to the table of the related data model.

![Diagram illustrating the relation between user and email records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733492/Medusa%20Book/one-to-one_cj5np3.jpg)

### One-sided One-to-One Relationship

In some use cases, you may want to define a one-to-one relationship only on one side. This means that the other data model does not have a relationship property pointing to the first one.

You can do this either from the `hasOne` or the `belongsTo` side.

#### hasOne Side

By default, the foreign key column is added to the table of the data model that has the `belongsTo` property. For example, if the `Email` data model belongs to the `User` data model, then the foreign key column is added to the `email` table.

If you want to define a one-to-one relationship only on the `User` data model's side (`hasOne` side), you can do so by passing the following properties to the second parameter of the `hasOne` method:

- `foreignKey`: A boolean indicating whether the foreign key column should be added to the table of the data model.
- `mappedBy`: Set to `undefined`, since the relationship is only defined on one side.

For example:

```ts highlights={oneToOneForeignKeyHighlights}
import { model } from "@medusajs/framework/utils"

const User = model.define("user", {
  id: model.id().primaryKey(),
  email: model.hasOne(() => Email, {
    foreignKey: true,
    mappedBy: undefined,
  }),
})

const Email = model.define("email", {
  id: model.id().primaryKey(),
})
```

In the example above, you add a one-to-one relationship from the `User` data model to the `Email` data model.

The foreign key column is added to the `user` table, and the `Email` data model does not have a relationship property pointing to the `User` data model.

#### belongsTo Side

To define the one-to-one relationship on the `belongsTo` side, pass `undefined` to the `mappedBy` property in the `belongsTo` method's second parameter.

For example:

```ts highlights={oneToOneUndefinedHighlights}
import { model } from "@medusajs/framework/utils"

const User = model.define("user", {
  id: model.id().primaryKey(),
})

const Email = model.define("email", {
  id: model.id().primaryKey(),
  user: model.belongsTo(() => User, {
    mappedBy: undefined,
  }),
})
```

In the example above, you add a one-to-one relationship from the `Email` data model to the `User` data model.

The `User` data model does not have a relationship property pointing to the `Email` data model.

***

## One-to-Many Relationship

A one-to-many relationship indicates that one record of a data model has many records of another data model.

To define a one-to-many relationship, create relationship properties in the data models using the following methods:

1. `hasMany`: indicates that the model has more than one record of the specified model.
2. `belongsTo`: indicates that the model belongs to one record of the specified model.

For example:

```ts highlights={oneToManyHighlights}
import { model } from "@medusajs/framework/utils"

const Store = model.define("store", {
  id: model.id().primaryKey(),
  products: model.hasMany(() => Product, {
    mappedBy: "store",
  }),
})

const Product = model.define("product", {
  id: model.id().primaryKey(),
  store: model.belongsTo(() => Store, {
    mappedBy: "products",
  }),
})
```

In this example, a store has many products, but a product belongs to one store.

### Optional Relationship

To make the relationship optional on the `belongsTo` side, use the `nullable` method on the property as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties#make-property-optional/index.html.md).

### One-to-Many Relationship in the Database

When you generate the migrations of data models that have a one-to-many relationship, the migration adds to the table of the data model that has the `belongsTo` property:

1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `product` table will have a `store_id` column.
2. A foreign key on the `{relation_name}_id` column to the table of the related data model.

![Diagram illustrating the relation between a store and product records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733937/Medusa%20Book/one-to-many_d6wtcw.jpg)

***

## Many-to-Many Relationship

A many-to-many relationship indicates that many records of a data model can be associated with many records of another data model.

To define a many-to-many relationship, create relationship properties in the data models using the `manyToMany` method.

For example:

```ts highlights={manyToManyHighlights}
import { model } from "@medusajs/framework/utils"

const Order = model.define("order", {
  id: model.id().primaryKey(),
  products: model.manyToMany(() => Product, {
    mappedBy: "orders",
    pivotTable: "order_product",
    joinColumn: "order_id",
    inverseJoinColumn: "product_id",
  }),
})

const Product = model.define("product", {
  id: model.id().primaryKey(),
  orders: model.manyToMany(() => Order, {
    mappedBy: "products",
  }),
})
```

The `manyToMany` method accepts two parameters:

1. A function that returns the associated data model.
2. An object of optional configuration. Only one of the data models in the relation can define the `pivotTable`, `joinColumn`, and `inverseJoinColumn` configurations, and it's considered the owner data model. The object can accept the following properties:
   - `mappedBy`: The name of the relationship property in the other data model. If not set, the property's name is inferred from the associated data model's name.
   - `pivotTable`: The name of the pivot table created in the database for the many-to-many relation. If not set, the pivot table is inferred by combining the names of the data models' tables in alphabetical order, separating them by `_`, and pluralizing the last name. For example, `order_products`.
   - `joinColumn`: The name of the column in the pivot table that points to the owner model's primary key.
   - `inverseJoinColumn`: The name of the column in the pivot table that points to the owned model's primary key.

The `pivotTable`, `joinColumn`, and `inverseJoinColumn` properties are only available after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7).

Following [Medusa v2.1.0](https://github.com/medusajs/medusa/releases/tag/v2.1.0), if `pivotTable`, `joinColumn`, and `inverseJoinColumn` aren't specified on either model, the owner is decided based on alphabetical order. So, in the example above, the `Order` data model would be the owner.

In this example, an order is associated with many products, and a product is associated with many orders. Since the `pivotTable`, `joinColumn`, and `inverseJoinColumn` configurations are defined on the order, it's considered the owner data model.

### Many-to-Many Relationship in the Database

When you generate the migrations of data models that have a many-to-many relationship, the migration adds a new pivot table. Its name is either the name you specify in the `pivotTable` configuration or the inferred name combining the names of the data models' tables in alphabetical order, separating them by `_`, and pluralizing the last name. For example, `order_products`.

The pivot table has a column with the name `{data_model}_id` for each of the data model's tables. It also has foreign keys on each of these columns to their respective tables.

The pivot table has columns with foreign keys pointing to the primary key of the associated tables. The column's name is either:

- The value of the `joinColumn` configuration for the owner table, and the `inverseJoinColumn` configuration for the owned table;
- Or the inferred name `{table_name}_id`.

![Diagram illustrating the relation between order and product records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726734269/Medusa%20Book/many-to-many_fzy5pq.jpg)

### Many-To-Many with Custom Columns

To add custom columns to the pivot table between two data models having a many-to-many relationship, you must define a new data model that represents the pivot table.

For example:

```ts highlights={manyToManyColumnHighlights}
import { model } from "@medusajs/framework/utils"

export const Order = model.define("order_test", {
  id: model.id().primaryKey(),
  products: model.manyToMany(() => Product, {
    pivotEntity: () => OrderProduct,
  }),
})

export const Product = model.define("product_test", {
  id: model.id().primaryKey(),
  orders: model.manyToMany(() => Order),
})

export const OrderProduct = model.define("orders_products", {
  id: model.id().primaryKey(),
  order: model.belongsTo(() => Order, {
    mappedBy: "products",
  }),
  product: model.belongsTo(() => Product, {
    mappedBy: "orders",
  }),
  metadata: model.json().nullable(),
})
```

The `Order` and `Product` data models have a many-to-many relationship. To add extra columns to the created pivot table, you pass a `pivotEntity` option to the `products` relation in `Order` (since `Order` is the owner). The value of `pivotEntity` is a function that returns the data model representing the pivot table.

The `OrderProduct` model defines, aside from the ID, the following properties:

- `order`: A relation that indicates this model belongs to the `Order` data model. You set the `mappedBy` option to the many-to-many relation's name in the `Order` data model.
- `product`: A relation that indicates this model belongs to the `Product` data model. You set the `mappedBy` option to the many-to-many relation's name in the `Product` data model.
- `metadata`: An extra column to add to the pivot table of type `json`. You can add other columns as well to the model.

***

## Cascades

When an operation is performed on a data model, such as record deletion, the relationship cascade specifies what related data model records should be affected by it.

For example, if a store is deleted, its products should also be deleted.

The `cascades` method used on a data model configures which child records an operation is cascaded to.

For example:

```ts highlights={highlights}
import { model } from "@medusajs/framework/utils"

const Store = model.define("store", {
  id: model.id().primaryKey(),
  products: model.hasMany(() => Product),
})
.cascades({
  delete: ["products"],
})

const Product = model.define("product", {
  id: model.id().primaryKey(),
  store: model.belongsTo(() => Store, {
    mappedBy: "products",
  }),
})
```

The `cascades` method accepts an object. Its key is the operation’s name, such as `delete`. The value is an array of relationship property names that the operation is cascaded to.

In the example above, when a store is deleted, its associated products are also deleted.


# Migrations

In this chapter, you'll learn what a migration is and how to generate a migration or write it manually.

## What is a Migration?

A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations.

The migration's file has a class with two methods:

- The `up` method reflects changes on the database.
- The `down` method reverts the changes made in the `up` method.

***

## Generate Migration

Instead of you writing the migration manually, the Medusa CLI tool provides a [db:generate](https://docs.medusajs.com/resources/medusa-cli/commands/db#dbgenerate/index.html.md) command to generate a migration for a modules' data models.

For example, assuming you have a `blog` Module, you can generate a migration for it by running the following command:

```bash
npx medusa db:generate blog
```

This generates a migration file under the `migrations` directory of the Blog Module. You can then run it to reflect the changes in the database as mentioned in [this section](#run-the-migration).

***

## Write a Migration Manually

You can also write migrations manually. To do that, create a file in the `migrations` directory of the module and in it, a class that has an `up` and `down` method. The class's name should be of the format `Migration{YEAR}{MONTH}{DAY}{HOUR}{MINUTE}.ts` to ensure migrations are ran in the correct order.

For example:

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 "@medusajs/framework/mikro-orm/migrations"

export class Migration202507021059 extends Migration {

  async up(): Promise<void> {
    this.addSql("create table if not exists \"author\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"author_pkey\" primary key (\"id\"));")
  }

  async down(): Promise<void> {
    this.addSql("drop table if exists \"author\" cascade;")
  }

}
```

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.

Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations.

### Migration File Naming

Migrations are executed in the ascending order of their file names. So, it's recommended to prefix the migration file name with the timestamp of when the migration was created. This ensures that migrations are executed in the order they were created.

For example, if you create a migration on July 2, 2025, at 10:59 AM, the file name should be `Migration202507021059_create_brand.ts`. This way, the migration will be executed after any previous migrations that were created before this date and time.

***

## Run the Migration

To run your migration, run the following command:

This command also syncs module links. If you don't want that, use the `--skip-links` option.

```bash
npx medusa db:migrate
```

This reflects the changes in the database as implemented in the migration's `up` method.

***

## Rollback the Migration

To rollback or revert the last migration you ran for a module, run the following command:

```bash
npx medusa db:rollback blog
```

This rolls back the last ran migration on the Blog Module.

### Caution: Rollback Migration before Deleting

If you need to delete a migration file, make sure to rollback the migration first. Otherwise, you might encounter issues when generating and running new migrations.

For example, if you delete the migration of the Blog Module, then try to create a new one, Medusa will create a brand new migration that re-creates the tables or indices. If those are still in the database, you might encounter errors.

So, always rollback the migration before deleting it.

***

## More Database Commands

To learn more about the Medusa CLI's database commands, refer to [this CLI reference](https://docs.medusajs.com/resources/medusa-cli/commands/db/index.html.md).

***

## Data Migration Scripts

In some use cases, you may need to perform data migration after updates to the database. For example, after you added a `Site` data model to the Blog Module, you want to assign all existing posts and authors to a default site. Another example is updating data stored in a third-party system.

In those scenarios, you can instead create a data migration script. They are asynchronous function that the Medusa application executes once when you run the `npx medusa db:migrate command`.

### How to Create a Data Migration Script

You can create data migration scripts in a TypeScript or JavaScript file under the `src/migration-scripts` directory. The file must export an asynchronous function that will be executed when the `db:migrate` command is executed.

For example, to create a data migration script for the Blog Module example, create the file `src/migration-scripts/migrate-blog-data.ts` with the following content:

```ts title="src/migration-scripts/migrate-blog-data.ts" highlights={dataMigrationHighlights}
import { MedusaModule } from "@medusajs/framework/modules-sdk"
import { ExecArgs } from "@medusajs/framework/types"
import { BLOG_MODULE } from "../modules/blog"
import { createWorkflow } from "@medusajs/framework/workflows-sdk"

export default async function migrateBlogData({ container }: ExecArgs) {
  // Check that the blog module exists
  if (!MedusaModule.isInstalled(BLOG_MODULE)) {
    return
  }

  await migrateBlogDataWorkflow(container).run({})
}

const migrateBlogDataWorkflow = createWorkflow(
  "migrate-blog-data",
  () => {
    // Assuming you have these steps
    createDefaultSiteStep()

    assignBlogDataToSiteStep()
  }
)
```

In the above example, you default export an asynchronous function that receives an object parameter with the [Medusa Container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) property.

In the function, you first ensure that the Blog Module is installed to avoid errors otherwise. Then, you run a workflow that you've created in the same file that performs the necessary data migration.

#### Test Data Migration Script

To test out the data migration script, run the migration command:

```bash
npx medusa db:migrate
```

Medusa will run any pending migrations and migration scripts, including your script.

If the script runs successfully, Medusa won't run the script again.

If there are errors in the script, you'll receive an error in the migration script logs. Medusa will keep running the script every time you run the migration command until it runs successfully.

***

## Migration Examples

The following section provides examples of writing migrations for common database changes.

### Example: Migration with Relationship

Consider you have a module with two data models: `Author` and `Post`. An author can have multiple posts, so there's a one-to-many relationship between the two models:

```ts
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id().primaryKey(),
  title: model.text(),
  author: model.belongsTo(() => Author, {
    mappedBy: "posts",
  }),
})

const Author = model.define("author", {
  id: model.id().primaryKey(),
  name: model.text(),
  posts: model.hasMany(() => Post, {
    mappedBy: "author",
  }),
})
```

To create a migration that reflects this relationship in the database, you can create a migration file as follows:

```ts title="src/modules/blog/migrations/Migration202507021200_create_author_and_post.ts"
import { Migration } from "@medusajs/framework/mikro-orm/migrations"

export class Migration20251230112505 extends Migration {

  override async up(): Promise<void> {
    this.addSql(`create table if not exists "author" ("id" text not null, "name" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "author_pkey" primary key ("id"));`)
    this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_author_deleted_at" ON "author" ("deleted_at") WHERE deleted_at IS NULL;`)

    this.addSql(`create table if not exists "post" ("id" text not null, "title" text not null, "author_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "post_pkey" primary key ("id"));`)
    this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_post_author_id" ON "post" ("author_id") WHERE deleted_at IS NULL;`)
    this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_post_deleted_at" ON "post" ("deleted_at") WHERE deleted_at IS NULL;`)

    this.addSql(`alter table if exists "post" add constraint "post_author_id_foreign" foreign key ("author_id") references "author" ("id") on update cascade;`)
  }

  override async down(): Promise<void> {
    this.addSql(`alter table if exists "post" drop constraint if exists "post_author_id_foreign";`)

    this.addSql(`drop table if exists "author" cascade;`)

    this.addSql(`drop table if exists "post" cascade;`)
  }

}
```

In this migration, the `up` method creates the `author` and `post` tables, including the foreign key constraint that establishes the relationship between them. The `down` method removes the foreign key constraint and drops both tables.

### Example: Migration to Add a New Column

Consider you have an existing `Post` data model, and you added a new `published_at` column to it.

You can create a migration file to add this new column as follows:

```ts title="src/modules/blog/migrations/Migration202507021300_add_published_at_to_post.ts"
import { Migration } from "@medusajs/framework/mikro-orm/migrations"

export class Migration20251230113025 extends Migration {

  override async up(): Promise<void> {
    this.addSql(`alter table if exists "post" add column if not exists "published_at" timestamptz null;`)
  }

  override async down(): Promise<void> {
    this.addSql(`alter table if exists "post" drop column if exists "published_at";`)
  }

}
```

In this migration, the `up` method adds the `published_at` column to the `post` table, while the `down` method removes it.

### Example: Migration to Remove a Column

Consider you have an existing `Post` data model, and you removed the `published_at` column from it.

You can create a migration file to remove this column as follows:

```ts title="src/modules/blog/migrations/Migration202507021400_remove_published_at_from_post.ts"
import { Migration } from "@medusajs/framework/mikro-orm/migrations"

export class Migration20251230113125 extends Migration {

  override async up(): Promise<void> {
    this.addSql(`alter table if exists "post" drop column if exists "published_at";`)
  }

  override async down(): Promise<void> {
    this.addSql(`alter table if exists "post" add column if not exists "published_at" timestamptz null;`)
  }

}
```

In this migration, the `up` method removes the `published_at` column from the `post` table, while the `down` method adds it back.

### Example: Migration to Rename a Column

Consider you have an existing `Post` data model with a `title` column, and you renamed it to `headline`.

You can create a migration file to rename this column as follows:

```ts title="src/modules/blog/migrations/Migration202507021500_rename_title_to_headline_in_post.ts"
import { Migration } from "@medusajs/framework/mikro-orm/migrations"

export class Migration20251230113214 extends Migration {

  override async up(): Promise<void> {
    this.addSql(`alter table if exists "post" rename column "title" to "headline";`)
  }

  override async down(): Promise<void> {
    this.addSql(`alter table if exists "post" rename column "headline" to "title";`)
  }

}
```

In this migration, the `up` method renames the `title` column to `headline` in the `post` table, while the `down` method renames it back to `title`.

### Example: Migration to Create an Index

Consider you have an existing `Post` data model, and add an index on the `headline` column to improve query performance:

```ts title="src/modules/blog/models/post.ts" highlights={indexHighlights}
import { model } from "@medusajs/framework/utils"
import { Author } from "./author"

export const Post = model.define("post", {
  id: model.id().primaryKey(),
  headline: model.text().index(),
  author: model.belongsTo(() => Author, {
    mappedBy: "posts",
  }),
})
```

You can create a migration file to create this index as follows:

```ts title="src/modules/blog/migrations/Migration202507021600_create_index_on_headline_in_post.ts"
import { Migration } from "@medusajs/framework/mikro-orm/migrations"

export class Migration20251230113322 extends Migration {

  override async up(): Promise<void> {
    this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_post_headline" ON "post" ("headline") WHERE deleted_at IS NULL;`)
  }

  override async down(): Promise<void> {
    this.addSql(`drop index if exists "IDX_post_headline";`)
  }

}
```

In this migration, the `up` method creates an index on the `headline` column of the `post` table, while the `down` method removes the index.

### Example: Migration to Drop an Index

Consider you have an existing `Post` data model with an index on the `headline` column, and you decide to remove this index.

You can create a migration file to drop this index as follows:

```ts title="src/modules/blog/migrations/Migration202507021700_drop_index_on_headline_in_post.ts"
import { Migration } from "@medusajs/framework/mikro-orm/migrations"

export class Migration20251230113350 extends Migration {

  override async up(): Promise<void> {
    this.addSql(`drop index if exists "IDX_post_headline";`)
  }

  override async down(): Promise<void> {
    this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_post_headline" ON "post" ("headline") WHERE deleted_at IS NULL;`)
  }

}
```

In this migration, the `up` method drops the index on the `headline` column of the `post` table, while the `down` method recreates the index.


# Environment Variables

In this chapter, you'll learn how environment variables are loaded in Medusa.

## System Environment Variables

The Medusa application loads and uses system environment variables.

For example, if you set the `PORT` environment variable to `8000`, the Medusa application runs on that port instead of `9000`.

In production, you should always use system environment variables that you set through your hosting provider.

***

## Environment Variables in .env Files

During development, it's easier to set environment variables in a `.env` file in your repository.

Based on your `NODE_ENV` system environment variable, Medusa will try to load environment variables from the following `.env` files:

As of [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0), `NODE_ENV` defaults to `production` when using `medusa start`. Otherwise, it defaults to `development`.

|\`.env\`|
|---|---|
|\`NODE\_ENV\`|\`.env\`|
|\`NODE\_ENV\`|\`.env.production\`|
|\`NODE\_ENV\`|\`.env.staging\`|
|\`NODE\_ENV\`|\`.env.test\`|

### Set Environment in `loadEnv`

In the `medusa-config.ts` file of your Medusa application, you'll find a `loadEnv` function used that accepts `process.env.NODE_ENV` as a first parameter.

This function is responsible for loading the correct `.env` file based on the value of `process.env.NODE_ENV`.

To ensure that the correct `.env` file is loaded as shown in the table above, only specify `development`, `production`, `staging` or `test` as the value of `process.env.NODE_ENV` or as the parameter of `loadEnv`.

***

## Environment Variables for Admin Customizations

Since the Medusa Admin is built on top of [Vite](https://vite.dev/), you prefix the environment variables you want to use in a widget or UI route with `VITE_`. Then, you can access or use them with the `import.meta.env` object.

Learn more in the [Admin Environment Variables](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md) chapter.

***

## Predefined Medusa Environment Variables

You can further customize the Medusa application behavior using the following predefined environment variables. You can set the environment variables in your `.env` file or in your deployment platform.

You should opt for setting configurations in `medusa-config.ts` where possible. For a full list of Medusa configurations, refer to the [Medusa Configurations chapter](https://docs.medusajs.com/learn/configurations/medusa-config/index.html.md).

|Environment Variable|Description|Default|
|---|---|---|
|\`HOST\`|The host to run the Medusa application on.|\`localhost\`|
|\`PORT\`|The port to run the Medusa application on.|\`9000\`|
|\`DATABASE\_URL\`|The URL to connect to the PostgreSQL database. Only used if |\`postgres://localhost/medusa-starter-default\`|
|\`STORE\_CORS\`|URLs of storefronts that can access the Medusa backend's Store APIs. Only used if |\`http://localhost:8000\`|
||URLs of admin dashboards that can access the Medusa backend's Admin APIs. Only used if |\`http://localhost:7000,http://localhost:7001,http://localhost:5173\`|
||URLs of clients that can access the Medusa backend's authentication routes. Only used if |\`http://localhost:7000,http://localhost:7001,http://localhost:5173\`|
||A random string used to create authentication tokens in the http layer. Only used if |-|
|\`COOKIE\_SECRET\`|A random string used to create cookie tokens in the http layer. Only used if |-|
|\`MEDUSA\_BACKEND\_URL\`|The URL to the Medusa backend. Only used if |-|
|\`DB\_HOST\`|The host for the database. It's used when generating migrations for a plugin, and when running integration tests.|\`localhost\`|
|\`DB\_USERNAME\`|The username for the database. It's used when generating migrations for a plugin, and when running integration tests.|-|
|\`DB\_PASSWORD\`|The password for the database user. It's used when generating migrations for a plugin, and when running integration tests.|-|
|\`DB\_TEMP\_NAME\`|The database name to create for integration tests.|-|
|\`LOG\_LEVEL\`|The allowed levels to log. Learn more in the |\`silly\`|
|\`LOG\_FILE\`|The file to save logs in. By default, logs aren't saved in any file. Learn more in the |-|
|\`MEDUSA\_DISABLE\_TELEMETRY\`|Whether to disable analytics data collection. Learn more in the |-|
|\`ADMIN\_AUTH\_TYPE\`|A string indicating the authentication method that the JS SDK instance uses in the Medusa Admin. Possible values are |\`session\`|
|\`ADMIN\_JWT\_TOKEN\_STORAGE\_KEY\`|A string indicating the key used to store the authentication JWT token in the browser's local storage. Only applicable if |\`medusa\_auth\_token\`|


# Event Data Payload

In this chapter, you'll learn how subscribers receive an event's data payload.

## Access Event's Data Payload

When events are emitted, they’re emitted with a data payload.

The object that the subscriber function receives as a parameter has an `event` property, which is an object holding the event payload in a `data` property with additional context.

For example:

```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-5" expandButtonLabel="Show Imports"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"

export default async function productCreateHandler({
  event,
}: SubscriberArgs<{ id: string }>) {
  const productId = event.data.id
  console.log(`The product ${productId} was created`)
}

export const config: SubscriberConfig = {
  event: "product.created",
}
```

The `event` object has the following properties:

- data: (\`object\`) The data payload of the event. Its properties are different for each event.
- name: (string) The name of the triggered event.
- metadata: (\`object\`) Additional data and context of the emitted event.

This logs the product ID received in the `product.created` event’s data payload to the console.

{/* ---

## List of Events with Data Payload

Refer to [this reference](!resources!/references/events) for a full list of events emitted by Medusa and their data payloads. */}


# Emit Workflow and Service Events

In this chapter, you'll learn about event types and how to emit an event in a service or workflow.

## Event Types

In your customization, you can emit an event, then listen to it in a subscriber and perform an asynchronus action, such as send a notification or data to a third-party system.

There are two types of events in Medusa:

1. Workflow event: an event that's emitted in a workflow after a commerce feature is performed. For example, Medusa emits the `order.placed` event after a cart is completed.
2. Service event: an event that's emitted to track, trace, or debug processes under the hood. For example, you can emit an event with an audit trail.

### Which Event Type to Use?

**Workflow events** are the most common event type in development, as most custom features and customizations are built around workflows.

Some examples of workflow events:

1. When a user creates a blog post and you're emitting an event to send a newsletter email.
2. When you finish syncing products to a third-party system and you want to notify the admin user of new products added.
3. When a customer purchases a digital product and you want to generate and send it to them.

You should only go for a **service event** if you're emitting an event for processes under the hood that don't directly affect front-facing features.

Some examples of service events:

1. When you're tracing data manipulation and changes, and you want to track every time some custom data is changed.
2. When you're syncing data with a search engine.

***

## Emit Event in a Workflow

To emit a workflow event, use the `emitEventStep` helper step provided in the `@medusajs/medusa/core-flows` package. It allows you to emit an event from within a workflow, and it only emits the event after the workflow has finished successfully.

For example:

```ts highlights={highlights}
import { 
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import {
  emitEventStep,
} from "@medusajs/medusa/core-flows"

const helloWorldWorkflow = createWorkflow(
  "hello-world",
  () => {
    // ...

    emitEventStep({
      eventName: "custom.created",
      data: {
        id: "123",
        // other data payload
      },
    })
  }
)
```

The `emitEventStep` accepts an object having the following properties:

- `eventName`: The event's name.
- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload.

In this example, you emit the event `custom.created` and pass in the data payload an ID property.

### Test it Out

If you execute the workflow, the event is emitted and you can see it in your application's logs.

Any subscribers listening to the event are executed.

### Emit Event Multiple Times

You can also emit the event multiple times with `emitEventStep` by passing an array of objects to the `data` property. The event is emitted once per each object in the array.

For example:

```ts highlights={highlights}
import { 
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import {
  emitEventStep,
} from "@medusajs/medusa/core-flows"

const helloWorldWorkflow = createWorkflow(
  "hello-world",
  () => {
    // ...

    emitEventStep({
      eventName: "custom.created",
      data: [
        { id: "123" },
        { id: "456" },
        { id: "789" },
      ],
    })
  }
)
```

The event `custom.created` will be emitted three times, once per each object in the array passed to the `data` property. The subscriber listening to the event will be executed three times, each time receiving one of the objects in the array as the event's payload.

***

## Emit Event in a Service

To emit a service event:

1. Resolve `event_bus` from the module's container in your service's constructor:

### Extending Service Factory

```ts title="src/modules/blog/service.ts" highlights={["9"]}
import { IEventBusService } from "@medusajs/framework/types"
import { MedusaService } from "@medusajs/framework/utils"

class BlogModuleService extends MedusaService({
  Post,
}){
  protected eventBusService_: AbstractEventBusModuleService

  constructor({ event_bus }) {
    super(...arguments)
    this.eventBusService_ = event_bus
  }
}
```

### Without Service Factory

```ts title="src/modules/blog/service.ts" highlights={["6"]}
import { IEventBusService } from "@medusajs/framework/types"

class BlogModuleService {
  protected eventBusService_: AbstractEventBusModuleService

  constructor({ event_bus }) {
    this.eventBusService_ = event_bus
  }
}
```

2. Use the event bus service's `emit` method in the service's methods to emit an event:

```ts title="src/modules/blog/service.ts" highlights={serviceHighlights}
class BlogModuleService {
  // ...
  performAction() {
    // TODO perform action

    this.eventBusService_.emit({
      name: "custom.event",
      data: {
        id: "123",
        // other data payload
      },
    })
  }
}
```

The method accepts an object having the following properties:

- `name`: The event's name.
- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload.

3. By default, the Event Module's service isn't injected into your module's container. To add it to the container, pass it in the module's registration object in `medusa-config.ts` in the `dependencies` property:

### Module Registration

```ts title="medusa-config.ts" highlights={depsHighlight}
import { Modules } from "@medusajs/framework/utils"

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/blog",
      dependencies: [
        Modules.EVENT_BUS,
      ],
    },
  ],
})
```

### Module Provider Registration

```ts title="medusa-config.ts" highlights={depsHighlight}
import { Modules } from "@medusajs/framework/utils"

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/payment",
      dependencies: [
        Modules.EVENT_BUS,
      ],
      options: {
        providers: [
          {
            resolve: "./src/modules/my-provider",
            id: "my-provider",
            options: {
              // ...
            },
          },
        ],
      },
    },
  ],
})
```

The `dependencies` property accepts an array of module registration keys. The specified modules' main services are injected into the module's container.

If a module has providers, the dependencies are also injected into the providers' containers.

### Test it Out

If you execute the `performAction` method of your service, the event is emitted and you can see it in your application's logs.

Any subscribers listening to the event are also executed.


# Events and Subscribers

In this chapter, you’ll learn about Medusa's event system, and how to handle events with subscribers.

## Handle Core Commerce Flows with Events

When building commerce digital applications, you'll often need to perform an action after a commerce operation is performed. For example, sending an order confirmation email when the customer places an order, or syncing data that's updated in Medusa to a third-party system.

Medusa emits events when core commerce features are performed, and you can listen to and handle these events in asynchronous functions. You can think of Medusa's events like you'd think about webhooks in other commerce platforms, but instead of having to setup separate applications to handle webhooks, your efforts only go into writing the logic right in your Medusa codebase.

You listen to an event in a subscriber, which is an asynchronous function that's executed when its associated event is emitted.

![A diagram showcasing an example of how an event is emitted when an order is placed.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732277948/Medusa%20Book/order-placed-event-example_e4e4kw.jpg)

Subscribers are useful to perform actions that aren't integral to the original flow. For example, you can handle the `order.placed` event in a subscriber that sends a confirmation email to the customer. The subscriber has no impact on the original order-placement flow, as it's executed outside of it.

If the action you're performing is integral to the main flow of the core commerce feature, use [workflow hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) instead.

### List of Emitted Events

Find a list of all emitted events in [this reference](https://docs.medusajs.com/resources/references/events/index.html.md).

***

## How to Create a Subscriber?

You create a subscriber in a TypeScript or JavaScript file under the `src/subscribers` directory. The file exports the function to execute and the subscriber's configuration that indicate what event(s) it listens to.

For example, create the file `src/subscribers/order-placed.ts` with the following content:

```ts title="src/subscribers/order-placed.ts"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation"

export default async function orderPlacedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const logger = container.resolve("logger")

  logger.info("Sending confirmation email...")

  await sendOrderConfirmationWorkflow(container)
    .run({
      input: {
        id: data.id,
      },
    })
}

export const config: SubscriberConfig = {
  event: `order.placed`,
}
```

This subscriber file exports:

- An asynchronous subscriber function that's executed whenever the associated event, which is `order.placed` is triggered.
- A configuration object with an `event` property whose value is the event the subscriber is listening to. You can also pass an array of event names to listen to multiple events in the same subscriber.

The subscriber function receives an object as a parameter that has the following properties:

- `event`: An object with the event's details. The `data` property contains the data payload of the event emitted, which is the order's ID in this case.
- `container`: The [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) that you can use to resolve registered resources.

In the subscriber function, you use the container to resolve the Logger utility and log a message in the console. Also, assuming you have a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that sends an order confirmation email, you execute it in the subscriber.

***

## Test the Subscriber

To test the subscriber, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, try placing an order either using Medusa's API routes or the [Next.js Starter Storefront](https://docs.medusajs.com/resources/nextjs-starter/index.html.md). You'll see the following message in the terminal:

```bash
info:    Processing order.placed which has 1 subscribers
Sending confirmation email...
```

The first message indicates that the `order.placed` event was emitted, and the second one is the message logged from the subscriber.

### Troubleshooting Subscribers

If your subscriber is not working as expected, refer to the [Subscriber Troubleshooting Guide](https://docs.medusajs.com/resources/troubleshooting/subscribers/not-working/index.html.md).

***

## Event Module

The subscription and emitting of events is handled by an Event Module, an Infrastructure Module that implements the pub/sub functionalities of Medusa's event system.

Medusa provides two Event Modules out of the box:

- [Local Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/local/index.html.md), used by default. It's useful for development, as you don't need additional setup to use it.
- [Redis Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md), which is useful in production. It uses [Redis](https://redis.io/) to implement Medusa's pub/sub events system.

Medusa's [architecture](https://docs.medusajs.com/learn/introduction/architecture/index.html.md) also allows you to build a custom Event Module that uses a different service or logic to implement the pub/sub system. Learn how to build an Event Module in [this guide](https://docs.medusajs.com/resources/infrastructure-modules/event/create/index.html.md).


# Framework Overview

In this chapter, you'll learn about the Medusa Framework and how it facilitates building customizations in your Medusa application.

## What is the Medusa Framework?

All commerce application require some degree of customization. So, it's important to choose a platform that facilitates building those customizations.

When you build customizations with other ecommerce platforms, they require you to pull data through HTTP APIs, run custom logic that span across systems in a separate application, and manually ensure data consistency across systems. This adds significant overhead and slows down development as you spend time managing complex distributed systems.

The Medusa Framework eliminates this overhead by providing powerful low-level APIs and tools that let you build any type of customization directly within your Medusa project. You can build custom features, orchestrate operations and query data seamlessy across systems, extend core functionality, and automate tasks in your Medusa application.

With the Medusa Framework, you can focus your efforts on building meaningful business customizations and continuously delivering new features.

Using the Medusa Framework, you can build customizations like:

- [Product Reviews](https://docs.medusajs.com/resources/how-to-tutorials/tutorials/product-reviews/index.html.md)
- [Deep integration with an ERP system](https://docs.medusajs.com/resources/recipes/erp/index.html.md)
- [CMS integration with seamless content retrieval](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md)
- [Custom item pricing in the cart](https://docs.medusajs.com/resources/examples/guides/custom-item-price/index.html.md)
- [Automated restock notifications](https://docs.medusajs.com/resources/recipes/commerce-automation/restock-notification/index.html.md)
- [Re-usable payment provider integrations](https://docs.medusajs.com/resources/references/payment/provider/index.html.md)

### Framework Concepts and Tools

- [Medusa Container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md)
- [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md)
- [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md)
- [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md)
- [Data Models](https://docs.medusajs.com/learn/fundamentals/data-models/index.html.md)
- [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md)
- [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md)
- [Events and Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md)
- [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md)
- [Plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md)

***

## Build Custom Features

The Medusa Framework allows you to build custom features tailored to your business needs.

To create a custom feature, you can create a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) that contains your feature's data models and the logic to manage them. A module is integrated into your Medusa application without side effects.

### Data Model

```ts highlights={modelHighlights}
import { model } from "@medusajs/framework/utils"

export const Post = model.define("post", {
  id: model.id().primaryKey(),
  title: model.text(),
})
```

### Service

```ts highlights={serviceHighlights}
import { MedusaService } from "@medusajs/framework/utils"
import { Post } from "./post"

export class BlogModuleService extends MedusaService({
  Post,
}){
  // CRUD methods generated by MedusaService
}
```

### Module Definition

```ts
import { Module } from "@medusajs/framework/utils"
import { BlogModuleService } from "./service"

export const BLOG_MODULE = "blog"

export default Module(BLOG_MODULE, {
  service: BlogModuleService,
})
```

Then, you can build commerce features and flows in [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that use your module. By using workflows, you benefit from features like [rollback mechanism](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md) and [retry configuration](https://docs.medusajs.com/learn/fundamentals/workflows/retry-failed-steps/index.html.md).

### Step

```ts highlights={stepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { BlogModuleService, BLOG_MODULE } from "../../modules/blog"

type Input = {
  title: string
}

const createPostStep = createStep(
  "create-post", 
  async (input: Input, { container }) => {
    const blogModuleService: BlogModuleService = container.resolve(
      BLOG_MODULE
    )

    const post = await blogModuleService.createPosts(input.title)
    
    return new StepResponse(post, post.id)
  },
  async (postId, { container }) => {
    if (!postId) {
      return
    }

    const blogModuleService: BlogModuleService = container.resolve(
      BLOG_MODULE
    )

    await blogModuleService.deletePosts(postId)
  }
)
```

### Workflow

```ts
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { createPostStep } from "./steps"

type Input = {
  title: string
}

export const createPostWorkflow = createWorkflow(
  "create-post",
  (input: Input) => {
    const post = createPostStep(input)

    return new WorkflowResponse(post)
  }
)
```

Finally, you can expose your custom feature with [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) that are built on top of your module and workflows.

```ts title="API Route Example"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createPostWorkflow } from "../../../workflows/create-post"

type PostRequestBody = {
  title: string
}

export const POST = async (
  req: MedusaRequest<PostRequestBody>,
  res: MedusaResponse
) => {
  const { result } = await createPostWorkflow(req.scope)
    .run({
      input: result.validatedBody,
    })

  return res.json(result)
}
```

### Examples

The following tutorials are step-by-step guides that show you how to build custom features using the Medusa Framework.

- [Product Reviews](https://docs.medusajs.com/resources/how-to-tutorials/tutorials/product-reviews/index.html.md): Build a product reviews feature in your Medusa application.
- [Wishlist](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md): Build a wishlist feature in your Medusa application.

### Start Learning

To learn more about the different concepts useful for building custom features, check out the following chapters:

- [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md)
- [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md)
- [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md)

***

## Extend Existing Features

The Medusa Framework is flexible and extensible, allowing you to extend and build on top of existing models and features.

To associate new properties and relations with an existing model, you can create a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) with data models that define these additions. Then, you can define a [module link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) that associates two data models from separate modules.

### Module Link

```ts highlights={defineLinkHighlights}
import BrandModule from "../modules/brand"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    isList: true,
  },
  BrandModule.linkable.brand
)
```

### Data Model

```ts
import { model } from "@medusajs/framework/utils"

export const Brand = model.define("brand", {
  id: model.id().primaryKey(),
  name: model.text(),
})
```

### Service

```ts
import { MedusaService } from "@medusajs/framework/utils"
import { Brand } from "./models/brand"

class BrandModuleService extends MedusaService({
  Brand,
}) {

}

export default BrandModuleService
```

### Module Definition

```ts
import { Module } from "@medusajs/framework/utils"
import BrandModuleService from "./service"

export const BRAND_MODULE = "brand"

export default Module(BRAND_MODULE, {
  service: BrandModuleService,
})
```

Then, you can [hook into existing workflows](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) to perform custom actions as part of existing features and flows. For example, you can create a brand when a product is created.

```ts title="Workflow Hook Example" highlights={hookHighlights}
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
import { StepResponse } from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
import { LinkDefinition } from "@medusajs/framework/types"
import { BRAND_MODULE } from "../../modules/brand"
import BrandModuleService from "../../modules/brand/service"

createProductsWorkflow.hooks.productsCreated(
  (async ({ products, additional_data }, { container }) => {
    if (!additional_data?.brand_id) {
      return new StepResponse([], [])
    }

    const brandModuleService: BrandModuleService = container.resolve(
      BRAND_MODULE
    )
    
    const brand = await brandModuleService.createBrands({
      name: additional_data.brand_name,
    })
  })
)
```

You can also build custom workflows using your custom module and Medusa's modules, and use [existing workflows and steps](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) within your custom workflows.

### Examples

The following tutorials are step-by-step guides that show you how to extend existing features using the Medusa Framework.

- [Custom Item Pricing](https://docs.medusajs.com/resources/examples/guides/custom-item-price/index.html.md): Add products with custom items to the cart.
- [Loyalty Points System](https://docs.medusajs.com/resources/how-to-tutorials/tutorials/loyalty-points/index.html.md): Reward and allow customers to redeem loyalty points.

### Start Learning

To learn more about the different concepts useful for extending features, check out the following chapters:

- [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md)
- [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md)
- [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md)

***

## Integrate Third-Party Services

The Medusa Framework provides the tools and infrastructure to build a middleware solution for your commerce ecosystem. You can integrate third-party services, perform operations across systems, and query data from multiple sources.

### Orchestrate Operations Across Systems

The Medusa Framework solves one of the biggest hurdles for ecommerce platforms: orchestrating operations across systems. Medusa has a built-in durable execution engine to help complete tasks that span multiple systems.

You can integrate a third-party service in a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). This module provides an interface to perform operations with the third-party service.

### Service

```ts highlights={erpServiceHighlights}
type Options = {
  apiKey: string
}

export default class ErpModuleService {
  private options: Options
  private client

  constructor({}, options: Options) {
    this.options = options
    // TODO initialize client that connects to ERP
  }

  async getProducts() {
    // assuming client has a method to fetch products
    return this.client.getProducts()
  }

  // TODO add more methods
}
```

### Module Definition

```ts
import { Module } from "@medusajs/framework/utils"
import ErpModuleService from "./service"

export const ERP_MODULE = "erp"

export default Module(ERP_MODULE, {
  service: ErpModuleService,
})
```

Then, you can build [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that perform operations across systems. In the workflow, you can use your module to interact with the integrated third-party service.

For example, you can create a workflow that syncs products from your ERP system to your Medusa application.

### Workflow

```ts highlights={erpWorkflowHighlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  transform,
} from "@medusajs/framework/workflows-sdk"
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"

export const syncFromErpWorkflow = createWorkflow(
  "sync-from-erp",
  () => {
    const erpProducts = getProductsFromErpStep()

    const productsToCreate = transform({
      erpProducts,
    }, (data) => {
      // TODO prepare ERP products to be created in Medusa
      return data.erpProducts.map((erpProduct) => {
        return {
          title: erpProduct.title,
          external_id: erpProduct.id,
          variants: erpProduct.variants.map((variant) => ({
            title: variant.title,
            metadata: {
              external_id: variant.id,
            },
          })),
          // other data...
        }
      })
    })

    createProductsWorkflow.runAsStep({
      input: {
        products: productsToCreate,
      },
    })

    return new WorkflowResponse({
      erpProducts,
    })
  }
)
```

### Step

```ts
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { ERP_MODULE } from "../../modules/erp"
import { ErpModuleService } from "../../modules/erp/service"

const getProductsFromErpStep = createStep(
  "get-products-from-erp",
  async (_, { container }) => {
    const erpModuleService: ErpModuleService = container.resolve(
      ERP_MODULE
    )

    const products = await erpModuleService.getProducts()

    return new StepResponse(products)
  }
)
```

By using a workflow to manage operations across systems, you benefit from features like [rollback mechanism](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md), [background long-running execution](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md), [retry configuration](https://docs.medusajs.com/learn/fundamentals/workflows/retry-failed-steps/index.html.md), and more. This is essential for building a middleware solution that performs operations across systems, as you don't have to worry about data inconsistencies or failures.

You can then execute this workflow at a specific interval using [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) or when an event occurs using [events and subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). You can also expose its features to client applications using an [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md).

### Scheduled Job

```ts highlights={syncProductsJobHighlights}
import {
  MedusaContainer,
} from "@medusajs/framework/types"
import { syncFromErpWorkflow } from "../workflows/sync-from-erp"

export default async function syncProductsJob(container: MedusaContainer) {
  await syncFromErpWorkflow(container).run({})
}

export const config = {
  name: "daily-product-sync",
  schedule: "0 0 * * *", // Every day at midnight
}
```

### Event Subscriber

```ts highlights={productsCreatedHandlerHighlights}
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation"

export default async function productsCreatedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }[]>) {
  await syncFromErpWorkflow(container).run({})
}

export const config: SubscriberConfig = {
  event: `product.created`,
}
```

### API Route

```ts highlights={apiRouteHighlights}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { syncFromErpWorkflow } from "../../../workflows/sync-from-erp"

export const POST = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const { result } = await syncFromErpWorkflow(req.scope).run({})

  return res.status(200).json(result)
}
```

#### Examples

The following tutorials are step-by-step guides that show you how to orchestrate operations across third-party services using the Medusa Framework.

- [Migrate Data from Magento](https://docs.medusajs.com/resources/integrations/guides/magento/index.html.md): Migrate data from Magento to your Medusa application.
- [Integrate Third-Party Services](https://docs.medusajs.com/resources/integrations/index.html.md): Integrate CMS, Fulfillment, Payment, and other third-party services.

#### Start Learning

To learn more about the different concepts useful for integrating third-party services, check out the following chapters:

- [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md)
- [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md)
- [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md)
- [Events and Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md)
- [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md)

### Query Data Across Systems

Another essential feature for integrating third-party services is querying data across those systems efficiently.

The Framework allows you to build links not only between Medusa data models, but also virtual data models using [read-only module links](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md). You can build a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) that provides the logic to query data from a third-party service, then create a read-only link between an existing data model and a virtual one from the third-party service.

### Read-Only Link

```ts highlights={readOnlyLinkHighlights}
import BrandModule from "../modules/brand"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    field: "id",
  },
  {
    ...BrandModule.linkable.brand.id,
    primaryKey: "product_id",
  },
  {
    readOnly: true,
  }
)
```

### Module Service

```ts highlights={brandModuleService}
type BrandModuleOptions = {
  apiKey: string
}

export default class BrandModuleService {
  private client

  constructor({}, options: BrandModuleOptions) {
    this.client = new Client(options)
  }

  async list(
    filter: {
      id: string | string[]
    }
  ) {
    return this.client.getBrands(filter)
    /**
     - Example of returned data:
     - 
     - [
     -   {
     -     "id": "brand_123",
     -     "name": "Brand 123",
     -     "product_id": "prod_321"
     -   },
     -   {
     -     "id": "post_456",
     -     "name": "Brand 456",
     -     "product_id": "prod_654"
     -   }
     - ]
    */
  }
}
```

### Module Definition

```ts
import { Module } from "@medusajs/framework/utils"
import BrandModuleService from "./service"

export const BRAND_MODULE = "brand"

export default Module(BRAND_MODULE, {
  service: BrandModuleService,
})
```

Then, you can use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) to retrieve a product and its brand from the third-party service in a single query.

```ts title="Query Example" highlights={queryHighlights}
const { result } = await query.graph({
  entity: "product",
  fields: ["id", "brand.*"],
  filters: {
    id: "prod_123",
  },
})

// result = [{
//   id: "prod_123",
//   brand: {
//     id: "brand_123",
//     name: "Brand 123",
//     product_id: "prod_123"
//   }
//   ...
// }]
```

Query simplifies the process of retrieving data across systems, as you can retrieve data from multiple sources in a single query.

#### Examples

The following tutorials are step-by-step guides that show you how to query data across systems using the Medusa Framework.

- [Integrate Sanity CMS](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md): Query data from third-party services using read-only links.

#### Start Learning

To learn more about the different concepts useful for querying data across systems, check out the following chapters:

- [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md)
- [Read-Only Links](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md)
- [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md)

***

## Automate Tasks

The Medusa Framework provides the tools to automate tasks in your Medusa application. Automation is useful when you want to perform a task periodically, such as syncing data, or when an event occurs, such as sending a confirmation email when an order is placed.

To build the task to be automated, you first create a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that contains the task's logic, such as syncing data or sending an email.

### Step

```ts
import { Modules } from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { CreateNotificationDTO } from "@medusajs/framework/types"

export const sendNotificationStep = createStep(
  "send-notification",
  async (data: CreateNotificationDTO[], { container }) => {
    const notificationModuleService = container.resolve(
      Modules.NOTIFICATION
    )
    const notification = await notificationModuleService.createNotifications(
      data
    )
    return new StepResponse(notification)
  }
)
```

### Workflow

```ts
import { 
  createWorkflow, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { sendNotificationStep } from "./steps/send-notification"

type WorkflowInput = {
  id: string
}

export const sendOrderConfirmationWorkflow = createWorkflow(
  "send-order-confirmation",
  ({ id }: WorkflowInput) => {
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "id",
        "email",
        "currency_code",
        "total",
        "items.*",
      ],
      filters: {
        id,
      },
    })
    
    const notification = sendNotificationStep([{
      to: orders[0].email,
      channel: "email",
      template: "order-placed",
      data: {
        order: orders[0],
      },
    }])

    return new WorkflowResponse(notification)
  }
)
```

Then, you can execute this workflow when an event occurs using a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), or at a specific interval using a [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md).

### Event Subscriber

```ts highlights={orderPlacedHandlerHighlights}
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation"

export default async function orderPlacedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await sendOrderConfirmationWorkflow(container)
    .run({
      input: {
        id: data.id,
      },
    })
}

export const config: SubscriberConfig = {
  event: "order.placed",
}
```

### Scheduled Job

```ts highlights={orderConfirmationJobHighlights}
import type {
  MedusaContainer,
} from "@medusajs/framework/types"
import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation"

export default async function orderConfirmationJob(
  container: MedusaContainer
) {
  await sendOrderConfirmationWorkflow(container).run({
    input: {
      id: "order_123",
    },
  })
}
export const config = {
  name: "order-confirmation-job",
  schedule: "0 0 * * *", // Every day at midnight
}
```

### Examples

The following guides are step-by-step guides that show you how to automate tasks using the Medusa Framework.

- [Restock Notifications](https://docs.medusajs.com/resources/recipes/commerce-automation/restock-notification/index.html.md): Send restock notifications to customers when a product is back in stock.
- [Sync Data from and to ERP](https://docs.medusajs.com/resources/recipes/erp#sync-products-from-erp/index.html.md): Sync data between your Medusa application and an ERP system.

### Start Learning

To learn more about the different concepts useful for automating tasks, check out the following chapters:

- [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md)
- [Events and Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md)
- [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md)

***

## Re-Use Customizations Across Applications

If you have custom features that you want to re-use across multiple Medusa applications, or you want to publish your customizations for the community to use, you can build a [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md).

A plugin encapsulates your customizations in a single package. The customizations include [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), and more.

![Diagram showcasing a wishlist plugin installed in a Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540762/Medusa%20Book/plugin-diagram_oepiis.jpg)

You can then publish that plugin to NPM and install it in any Medusa application. This allows you to re-use your customizations efficiently across multiple projects, or share them with the community.

### Examples

The following tutorials are step-by-step guides that show you how to build plugins using the Medusa Framework.

- [Wishlist Plugin](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md): Build a wishlist plugin for your Medusa application.
- [Migrate Data from Magento Plugin](https://docs.medusajs.com/resources/integrations/guides/magento/index.html.md): Build a plugin that migrates data from Magento to your Medusa application.

### Start Learning

To learn more about the different concepts useful for building plugins, check out the following chapters:

- [Plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md)


# Automatically Generated Types

In this chapter, you'll learn about the types Medusa automatically generates under the `.medusa` directory and how you should use them.

## What are Automatically Generated Types?

Medusa automatically generates TypeScript types for:

1. Data models collected in the [Query's graph](https://docs.medusajs.com/learn/fundamentals/module-links/query#querying-the-graph/index.html.md). These types provide you with auto-completion and type checking when using Query.
   - Generated data model types are located in the `.medusa/types/query-entry-points.d.ts` file.
2. Modules registered in the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md). These types provide you with auto-completion and type checking when resolving modules from the container.
   - Generated module registration names are located in the `.medusa/types/modules-bindings.d.ts` file.

![Diagram showcasing the directory structure of the .medusa directory, highlighting the types folder and its contents.](https://res.cloudinary.com/dza7lstvk/image/upload/v1753448927/Medusa%20Book/generated-types-dir_bmvdts.jpg)

***

## How to Trigger Type Generation?

As of [Medusa v2.12.4](https://github.com/medusajs/medusa/releases/tag/v2.12.4), types are generated when you run the `build` command. Prior versions only generated types when running the `dev` command.

The Medusa application generates these types automatically when you run the `build` or `dev` commands:

```bash npm2yarn
npm run build
```

So, if you add a new data model or module and you don't find it in auto-completion or type checking, you can run the `build` command to regenerate the types.

### How to Generate Types for Local Plugins?

This feature is available as of [Medusa v2.12.4](https://github.com/medusajs/medusa/releases/tag/v2.12.4).

Local plugins are plugins installed in your Medusa application with the `plugin:develop` command. To generate types for those plugins, run the `dev` command in the Medusa application:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

Medusa will copy the generated types under the `.medusa/types` directory of the application to the local plugin's directory.

***

## How to Use the Generated Types?

The generated types are only meant for auto-completion and type checking.

For example, consider you have a Brand Module with a `Brand` data model. Due to the auto-generated types, you can do the following:

### Query

```ts
const { data: [brand] } = await query.graph({
  entity: "brand",
  fields: ["*"],
  filters: {
    id: "brand_123",
  },
})

// brands has the correct type, so you can access its properties with auto-completion and type checking
console.log(brand.name)
```

### Container

```ts
const brandModuleService = req.scope.resolve("brand")

// brandModuleService has the correct type, so you can access its methods with auto-completion and type checking
const brand = await brandModuleService.retrieveBrand("brand_123")
```

### Don't Import the Generated Types

The generated types are only meant to help you in your development process by providing auto-completion and type checking. You should not import them directly in your code.

Since you don't commit the `.medusa` directory to your version control system or your production environment, importing these types may lead to build errors.

Instead, if you need to use a data model's type in your customizations, you can use the [InferTypeOf](https://docs.medusajs.com/learn/fundamentals/data-models/infer-type/index.html.md) utility function, which infers the type of a data model based on its properties.

For example, if you want to use the `Brand` data model's type in your customizations, you can do the following:

```ts
import { InferTypeOf } from "@medusajs/framework/types"
import { Brand } from "../modules/brand/models/brand" // relative path to the model

export type BrandType = InferTypeOf<typeof Brand>
```

You can then use the `BrandType` type in your customizations, such as in workflow inputs or service method outputs.


# Medusa Container

In this chapter, you’ll learn about the Medusa container and how to use it.

## What is the Medusa Container?

The Medusa container is a registry of Framework and commerce tools that's accessible across your application. Medusa automatically registers these tools in the container, including custom ones that you've built, so that you can use them in your customizations.

In other platforms, if you have a resource A (for example, a class) that depends on a resource B, you have to manually add resource B to the container or specify it beforehand as A's dependency, which is often done in a file separate from A's code. This becomes difficult to manage as you maintain larger applications with many changing dependencies.

Medusa simplifies this process by giving you access to the container, with the tools or resources already registered, at all times in your customizations. When you reach a point in your code where you need a tool, you resolve it from the container and use it.

For example, consider you're creating an API route that retrieves products based on filters using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md), a tool that fetches data across the application. In the API route's function, you can resolve Query from the container passed to the API route and use it:

```ts highlights={highlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const query = req.scope.resolve("query")

  const { data: products } = await query.graph({
    entity: "product",
    fields: ["*"],
    filters: {
      id: "prod_123",
    },
  })

  res.json({
    products,
  })
}
```

The API route accepts as a first parameter a request object that has a `scope` property, which is the Medusa container. It has a `resolve` method that resolves a resource from the container by the key it's registered with.

You can learn more about how Query works in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md).

***

## List of Resources Registered in the Medusa Container

Find a full list of the registered resources and their registration key in [this reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md)

***

## How to Resolve From the Medusa Container

This section gives quick examples of how to resolve resources from the Medusa container in customizations other than an API route, which is covered in the section above.

### Subscriber

A [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) function, which is executed when an event is emitted, accepts as a parameter an object with a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key:

```ts highlights={subscriberHighlights}
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export default async function productCreateHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const query = container.resolve(ContainerRegistrationKeys.QUERY)

  const { data: products } = await query.graph({
    entity: "product",
    fields: ["*"],
    filters: {
      id: data.id,
    },
  })

  console.log(`You created a product with the title ${products[0].title}`)
}

export const config: SubscriberConfig = {
  event: `product.created`,
}
```

### Scheduled Job

A [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) function, which is executed at a specified interval, accepts the Medusa container as a parameter. Use its `resolve` method to resolve a resource by its registration key:

```ts highlights={scheduledJobHighlights}
import { MedusaContainer } from "@medusajs/framework/types"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const query = container.resolve(ContainerRegistrationKeys.QUERY)

  const { data: products } = await query.graph({
    entity: "product",
    fields: ["*"],
    filters: {
      id: "prod_123",
    },
  })

  console.log(
    `You have ${products.length} matching your filters.`
  )
}

export const config = {
  name: "every-minute-message",
  // execute every minute
  schedule: "* * * * *",
}
```

### Workflow Step

A [step in a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), which is a special function where you build durable execution logic across multiple modules, accepts in its second parameter a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key:

```ts highlights={workflowStepsHighlight}
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

const step1 = createStep("step-1", async (_, { container }) => {
  const query = container.resolve(ContainerRegistrationKeys.QUERY)

  const { data: products } = await query.graph({
    entity: "product",
    fields: ["*"],
    filters: {
      id: "prod_123",
    },
  })

  return new StepResponse(products)
})
```

### Module Services and Loaders

A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), which is a package of functionalities for a single feature or domain, has its own container, so it can't resolve resources from the Medusa container.

Learn more about the module's container in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md).

***

## Container Registration Keys Auto-Completion

When you resolve a resource from the Medusa container, you can use auto-completion to find the registration key of the resource.

This is possible due to Medusa's auto-generated types, which are generated in the `.medusa` directory when you run the `dev` command.

Learn more in the [Auto-Generated Types](https://docs.medusajs.com/learn/fundamentals/generated-types/index.html.md) chapter.


# Add Columns to a Link Table

In this chapter, you'll learn how to add custom columns to a link definition's table and manage them.

## Link Table's Default Columns

When you define a link between two data models, Medusa creates a link table in the database to store the IDs of the linked records. You can learn more about the created table in the [Module Links chapter](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md).

In various cases, you might need to store additional data in the link table. For example, if you define a link between a `product` and a `post`, you might want to store the publish date of the product's post in the link table.

In those cases, you can add a custom column to a link's table in the link definition. You can later set that column whenever you create or update a link between the linked records.

***

## How to Add Custom Columns to a Link's Table?

The `defineLink` function used to define a link accepts a third parameter, which is an object of options.

To add custom columns to a link's table, pass in the third parameter of `defineLink` a `database` property:

```ts highlights={linkHighlights}
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  ProductModule.linkable.product,
  BlogModule.linkable.blog,
  {
    database: {
      extraColumns: {
        metadata: {
          type: "json",
        },
      },
    },
  }
)
```

This adds to the table created for the link between `product` and `blog` a `metadata` column of type `json`.

### Database Options

The `database` property defines configuration for the table created in the database.

Its `extraColumns` property defines custom columns to create in the link's table.

`extraColumns`'s value is an object whose keys are the names of the columns, and values are the column's configurations as an object.

### Column Configurations

The column's configurations object accepts the following properties:

- `type`: The column's type. Possible values are:
  - `string`
  - `text`
  - `integer`
  - `boolean`
  - `date`
  - `time`
  - `datetime`
  - `enum`
  - `json`
  - `array`
  - `enumArray`
  - `float`
  - `double`
  - `decimal`
  - `bigint`
  - `mediumint`
  - `smallint`
  - `tinyint`
  - `blob`
  - `uuid`
  - `uint8array`
- `defaultValue`: The column's default value.
- `nullable`: Whether the column can have `null` values.

***

## Set Custom Column when Creating Link

The object you pass to Link's `create` method accepts a `data` property. Its value is an object whose keys are custom column names, and values are the value of the custom column for this link.

For example:

Refer to the [Link](https://docs.medusajs.com/learn/fundamentals/module-links/link/index.html.md) chapter to learn how to import and use the `link` instance.

### Link

```ts
await link.create({
  [Modules.PRODUCT]: {
    product_id: "123",
  },
  [BLOG_MODULE]: {
    post_id: "321",
  },
  data: {
    metadata: {
      test: true,
    },
  },
})
```

### createRemoteLinksStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { BLOG_MODULE } from "../modules/blog"
import { createRemoteLinksStep } from "@medusajs/medusa/core-flows"
import { 
  createWorkflow,
  transform,
} from "@medusajs/framework/workflows-sdk"

export const myWorkflow = createWorkflow(
  "my-workflow",
  () => {
    // ...
    const linkData = transform({
      // TODO pass input data
    }, () => {
      return [
        {
          [Modules.PRODUCT]: {
            product_id: "123",
          },
          [BLOG_MODULE]: {
            post_id: "321",
          },
          data: {
            metadata: {
              test: true,
            },
          },
        },
      ]
    })
    createRemoteLinksStep(linkData)
    // ...
  }
)
```

***

## Retrieve Custom Column with Link

To retrieve linked records with their custom columns, use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). This section explores two methods to retrieve a link's custom columns.

### Method 1: Using Special Link Field

A data model will have a special field for all its link tables. The field's name is in the `{camel_case_data_model_name}_link` format, where `{camel_case_data_model_name}` is the name of the linked data model in camel case.

For example:

```ts highlights={method1Highlights}
const { data } = await query.graph({
  entity: "product",
  fields: ["id", "title", "post_link.metadata", "post.*"],
  filters: {
    id: "prod_123",
  },
})
```

In this example, you retrieve a product and the `metadata` custom column in the link table between `product` and `post` using the `post_link.metadata` field.

You can also retrieve all custom columns in the link table using the `post_link.*` field.

### Method 2: Using Entry Point

A module link's definition, exported by a file under `src/links`, has a special `entryPoint` property. Use this property when specifying the `entity` property in Query's `graph` method.

For example:

```ts highlights={retrieveHighlights}
import productPostLink from "../links/product-post"

// ...

const { data } = await query.graph({
  entity: productPostLink.entryPoint,
  fields: ["metadata", "product.*", "post.*"],
  filters: {
    product_id: "prod_123",
  },
})
```

This retrieves the product of id `prod_123` and its linked `post` records.

In the `fields` array you pass `metadata`, which is the custom column to retrieve of the link.

***

## Update Custom Column's Value

Link's `create` method updates a link's data if the link between the specified records already exists.

So, to update the value of a custom column in a created link, use the `create` method again passing it a new value for the custom column.

For example:

### Link

```ts
await link.create({
  [Modules.PRODUCT]: {
    product_id: "123",
  },
  [BLOG_MODULE]: {
    post_id: "321",
  },
  data: {
    metadata: {
      test: false,
    },
  },
})
```

### updateRemoteLinksStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { BLOG_MODULE } from "../modules/blog"
import { updateRemoteLinksStep } from "@medusajs/medusa/core-flows"
import { 
  createWorkflow,
  transform,
} from "@medusajs/framework/workflows-sdk"

export const myWorkflow = createWorkflow(
  "my-workflow",
  () => {
    // ...
    const linkData = transform({
      // TODO pass input data
    }, () => {
      return [
        {
          [Modules.PRODUCT]: {
            product_id: "123",
          },
          [BLOG_MODULE]: {
            post_id: "321",
          },
          data: {
            metadata: {
              test: false,
            },
          },
        },
      ]
    })
    updateRemoteLinksStep(linkData)
    // ...
  }
)
```


# Module Link Direction

In this chapter, you'll learn about the difference in module link directions, and which to use based on your use case.

The details in this chapter don't apply to [Read-Only Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md). Refer to the [Read-Only Module Links chapter](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md) for more information on read-only links and their direction.

## Link Direction

The module link's direction depends on the order you pass the data model configuration parameters to `defineLink`.

For example, the following defines a link from the Blog Module's `post` data model to the Product Module's `product` data model:

```ts
export default defineLink(
  BlogModule.linkable.post,
  ProductModule.linkable.product
)
```

Whereas the following defines a link from the Product Module's `product` data model to the Blog Module's `post` data model:

```ts
export default defineLink(
  ProductModule.linkable.product,
  BlogModule.linkable.post
)
```

The above links are two different links that serve different purposes.

***

## Which Link Direction to Use?

### Extend Data Models

If you're adding a link to a data model to extend it and add new fields, define the link from the main data model to the custom data model.

For example, consider you want to add a `subtitle` custom field to the `product` data model. To do that, you define a `Subtitle` data model in your module, then define a link from the `Product` data model to it:

```ts
export default defineLink(
  ProductModule.linkable.product,
  BlogModule.linkable.subtitle
)
```

### Associate Data Models

If you're linking data models to indicate an association between them, define the link from the custom data model to the main data model.

For example, consider you have `Post` data model representing a blog post, and you want to associate a blog post with a product. To do that, define a link from the `Post` data model to `Product`:

```ts
export default defineLink(
  BlogModule.linkable.post,
  ProductModule.linkable.product
)
```


# Index Module

In this chapter, you'll learn about the Index Module and how you can use it.

The Index Module is experimental and still in development, so it is subject to change. Consider whether your application can tolerate minor issues before using it in production.

## What is the Index Module?

The Index Module is a tool to perform high-performance queries across modules, for example, to filter linked modules.

While modules share the same database by default, Medusa [isolates modules](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md) to allow using external data sources or different database types.

So, when you retrieve data across modules using Query, Medusa aggregates the data coming from different modules to create the end result. This approach limits your ability to filter data by linked modules. For example, you can't filter products (created in the Product Module) by their brand (created in the Brand Module).

The Index Module solves this problem by ingesting data into a central data store on application startup. The data store has a relational structure that enables efficient filtering of data ingested from different modules (and their data stores). So, when you retrieve data with the Index Module, you're retrieving it from the Index Module's data store, not the original data source.

![Diagram showcasing how data is retrieved from the Index Module's data store](https://res.cloudinary.com/dza7lstvk/image/upload/v1747988533/Medusa%20Book/index-module_epurmt.jpg)

### Ingested Data Models

Data models are ingested if they're linked to other data models with the `filterable` property set, as you'll learn in the [Ingest Custom Data Models](#how-to-ingest-custom-data-models) section. Medusa also ingests some core data models by default, such as `Product`, `ProductVariant`, `Price`, and `SalesChannel`.

Prior to [Medusa v2.10.2](https://github.com/medusajs/medusa/releases/tag/v2.10.2), only the `Product`, `ProductVariant`, `Price`, and `SalesChannel` data models were ingested. Make sure to update to the latest version to ingest all core data models.

***

## How to Install the Index Module

To install the Index Module, run the following command in your Medusa project to install its package:

```bash npm2yarn
npm install @medusajs/index
```

Then, add the Index Module to your Medusa configuration in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    // ...
    {
      resolve: "@medusajs/index",
    },
  ],
})
```

Finally, run the migrations to create the necessary tables for the Index Module in your database:

```bash
npx medusa db:migrate
```

### Ingest Data

The Index Module only ingests data when you start your Medusa server. So, to [ingest data models](#ingested-data-models), start the Medusa application:

```bash npm2yarn
npm run dev
```

The ingestion process may take a while if your product catalog is large. You'll see the following messages in the logs:

```bash
info:    [Index engine] Checking for index changes
info:    [Index engine] Found 7 index changes that are either pending or processing
info:    [Index engine] syncing entity 'ProductVariant'
info:    [Index engine] syncing entity 'ProductVariant' done (+38.73ms)
info:    [Index engine] syncing entity 'Product'
info:    [Index engine] syncing entity 'Product' done (+18.21ms)
info:    [Index engine] syncing entity 'LinkProductVariantPriceSet'
info:    [Index engine] syncing entity 'LinkProductVariantPriceSet' done (+33.87ms)
info:    [Index engine] syncing entity 'Price'
info:    [Index engine] syncing entity 'Price' done (+22.79ms)
info:    [Index engine] syncing entity 'PriceSet'
info:    [Index engine] syncing entity 'PriceSet' done (+10.72ms)
info:    [Index engine] syncing entity 'LinkProductSalesChannel'
info:    [Index engine] syncing entity 'LinkProductSalesChannel' done (+11.45ms)
info:    [Index engine] syncing entity 'SalesChannel'
info:    [Index engine] syncing entity 'SalesChannel' done (+7.00ms)
```

### Update Index on Data Changes

The Index Module automatically updates its data store when data in the ingested data models change. So, you don't need to do anything to keep the data in sync.

For example, if you create a new product, the Index Module will ingest it into its data store.

### Enable Index Module Feature Flag

Since the Index Module is still experimental, the `/store/products` and `/admin/products` API routes will use the Index Module to retrieve products only if the Index Module's feature flag is enabled. By enabling the feature flag, you can filter products by their linked data models in these API routes.

To enable the Index Module's feature flag, add the following line to your `.env` file:

```env
MEDUSA_FF_INDEX_ENGINE=true
```

If you send a request to the `/store/products` or `/admin/products` API routes, you'll receive the following response:

```json
{
  "products": [
    // ...
  ],
  "count": 2,
  "estimate_count": 2,
  "offset": 0,
  "limit": 50
}
```

Notice the `estimate_count` property, which is the estimated total number of products in the database. You'll learn more about it in the [Pagination](#apply-pagination-with-the-index-module) section.

***

## How to Use the Index Module

The Index Module adds a new `index` method to [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) and it has the same API as the `graph` method.

For example, to filter products by a sales channel ID:

```ts title="src/api/custom/products/route.ts" highlights={basicHighlights}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve("query")
  
  const { data: products } = await query.index({
    entity: "product",
    fields: ["*", "sales_channels.*"],
    filters: {
      sales_channels: {
        id: "sc_123",
      },
    },
  })

  res.json({ products })
}
```

This will return all products that are linked to the sales channel with the ID `sc_123`.

The `index` method accepts an object with the same properties as the `graph` method's parameter:

- `entity`: The data model's name, as specified in the first parameter of the `model.define` method used for the data model's definition.
- `fields`: An array of the data model’s properties, relations, and linked data models to retrieve in the result.
- `filters`: An object with the filters to apply on the data model's properties, relations, and linked data models that are ingested.

***

## How to Ingest Custom Data Models

You can ingest core and custom data models into the Index Module. You can do so by defining a link between your custom data model and one of the core data models, and setting the `filterable` property in the link definition.

Read-only links are not supported by the Index Module.

For example, assuming you have a Brand Module with a Brand data model (as explained in the [Customizations](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)), you can ingest it into the Index Module using the `filterable` property in its link definition to the `Product` data model:

```ts title="src/links/product-brand.ts" highlights={filterableHighlights}
import BrandModule from "../modules/brand"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    isList: true,
  },
  {
    linkable: BrandModule.linkable.brand,
    filterable: ["id", "name"],
  }
)
```

The `filterable` property is an array of property names in the data model that can be filtered using the `index` method. When the `filterable` property is set, the Index Module will ingest into its data store the custom data model.

But first, you must run the migrations to sync the link, then start the Medusa application:

```bash npm2yarn
npx medusa db:migrate
npm run dev
```

You'll then see the following message in the logs:

```bash
info:    [Index engine] syncing entity 'LinkProductProductBrandBrand'
info:    [Index engine] syncing entity 'LinkProductProductBrandBrand' done (+3.64ms)
info:    [Index engine] syncing entity 'Brand'
info:    [Index engine] syncing entity 'Brand' done (+0.99ms)
```

You can now filter products by their brand, and vice versa. For example:

```ts title="src/api/custom/products/route.ts"
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve("query")
  
  const { data: products } = await query.index({
    entity: "product",
    fields: ["*", "brand.*"],
    filters: {
      brand: {
        name: "Acme",
      },
    },
  })

  res.json({ products })
}
```

This will return all products that are linked to the brand with the name `Acme`. For example:

```json title="Example Response"
{
  "products": [
    {
      "id": "prod_123",
      "brand": {
        "id": "brand_123",
        "name": "Acme"
      },
      // ...
    }
  ]
}
```

***

## Trigger Index Reingestion

The Index API routes are available from [Medusa v2.11.2](https://github.com/medusajs/medusa/releases/tag/v2.11.2).

Medusa provides API routes to view and trigger index reingestion or syncing. This is useful if you want to reingest data manually, for example, after a large data import.

Refer to the [Index API Reference](https://docs.medusajs.com/api/admin#index) for more information about the available API routes and how to use them.

***

## Apply Pagination with the Index Module

Similar to Query's `graph` method, the Index Module accepts a `pagination` object to paginate the results.

For example, to paginate the products and retrieve `10` products per page:

```ts title="src/api/custom/products/route.ts" highlights={paginationHighlights}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve("query")
  
  const { 
    data: products,
    metadata,
  } = await query.index({
    entity: "product",
    fields: ["*", "brand.*"],
    filters: {
      brand: {
        name: "Acme",
      },
    },
    pagination: {
      take: 10,
      skip: 0,
    },
  })

  res.json({ products, ...metadata })
}
```

The `pagination` object accepts the following properties:

- `take`: The number of items to retrieve per page.
- `skip`: The number of items to skip before retrieving the items.

When the `pagination` property is set, the `index` method will also return a `metadata` property. `metadata` is an object with the following properties:

- `skip`: The number of items skipped.
- `take`: The number of items retrieved.
- `estimate_count`: The estimated total number of items in the database matching the query. This value is retrieved from the PostgreSQL query planner rather than using a `COUNT` query, so it may not be accurate for smaller data sets.

For example, this is the response returned by the above API route:

```json title="Example Response"
{
  "products": [
    // ...
  ],
  "skip": 0,
  "take": 10,
  "estimate_count": 100
}
```

***

## Cache Index Module Results

### Prerequisites

- [Caching Module installed with a provider.](https://docs.medusajs.com/resources/infrastructure-modules/caching#install-the-caching-module/index.html.md)

Caching options are available from [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0).

You can cache Index Module results to improve performance and reduce database load. To do that, you can pass a `cache` property in the second parameter of the `query.index` method.

For example, to enable caching for a query:

```ts highlights={[["6", "enable", "Enable caching for this query."]]}
const { data: products } = await query.index({
  entity: "product",
  fields: ["id", "title"],
}, {
  cache: {
    enable: true,
  },
})
```

In this example, you enable caching of the query's results. The next time the same query is executed, the results are returned from the cache instead of querying the database.

Refer to the [Caching Module documentation](https://docs.medusajs.com/resources/infrastructure-modules/caching/concepts#caching-best-practices/index.html.md) for best practices on caching.

### Cache Properties

`cache` is an object that accepts the following properties:

- enable: (\`boolean\` | \`((args: any\[]) => boolean | undefined)\`) Whether to enable caching of query results. If a function is passed, it receives as a parameter the \`query.index\` parameters, and returns a boolean indicating whether caching is enabled.
- key: (\`string\` | \`((args: any\[], cachingModule: ICachingModuleService) => string | Promise\<string>)\`) The key to cache the query results with.  If no key is provided, the Caching Module will generate the key from the \`query.index\` parameters.

  If a function is passed, it receives the following properties:

  1\. The parameters passed to \`query.index\`.

  2\. The \[Caching Module's service]\(!resources!/references/caching-service), which you can use to perform caching operations.

  The function must return a string indicating the cache key.
- tags: (\`string\[]\` | \`((args: any\[]) => string\[] | undefined)\`) The tags to associate with the cached results. Tags are useful to group related items. If no tag is provided, the Caching Module will generate relevant tags based on the entity and its retrieved relations.

  If a function is passed, it receives as a parameter the \`query.index\` parameters, and returns an array of strings indicating the cache tags.
- ttl: (\`number\` | \`((args: any\[]) => number | undefined)\`) The time-to-live (TTL) for the cached results, in seconds. If no TTL is provided, the Caching Module Provider will receive the \[configured TTL of the Caching Module]\(!resources!/infrastructure-modules/caching#caching-module-options), or it will use its own default value.

  If a function is passed, it receives as a parameter the \`query.index\` parameters, and returns a number indicating the TTL.
- autoInvalidate: (\`boolean\` | \`((args: any\[]) => boolean | undefined)\`) Whether to automatically invalidate the cached data when it expires.

  If a function is passed, it receives as a parameter the \`query.index\` parameters, and returns a boolean indicating whether to automatically invalidate the cache.
- providers: (\`string\[]\` | \`((args: any\[]) => string\[] | undefined)\`) The IDs of the providers to use for caching. If not provided, the \[default Caching Module Provider]\(!resources!/infrastructure-modules/caching/providers#default-caching-module-provider) is used. If multiple providers are passed, the cache is stored and retrieved in those providers in order.

  If a function is passed, it receives as a parameter the \`query.index\` parameters, and return an array of strings indicating the providers to use.

### Set Cache Key

By default, the Caching Module generates a cache key for a query based on the arguments passed to `query.index`. The cache key is a unique key that the cached result is stored with.

Alternatively, you can set a custom cache key for a query. This is useful if you want to manage invalidating the cache manually.

To set the cache key of a query, pass the `cache.key` option:

```ts highlights={[["7"]]}
const { data: products } = await query.index({
  entity: "product",
  fields: ["id", "title"],
}, {
  cache: {
    enable: true,
    key: "products-123456",
    // to disable auto invalidation:
    // autoInvalidate: false,
  },
})
```

In the example above, you cache the query results with the `products-123456` key.

You should generate cache keys with the Caching Module service's [computeKey method](https://docs.medusajs.com/resources/references/caching-service#computeKey/index.html.md) to ensure that the key is unique and follows best practices.

You can also pass a function as the value of `cache.key`:

```ts highlights={[["7"], ["8"], ["9"], ["10"], ["11"]]}
const { data: products } = await query.index({
  entity: "product",
  fields: ["id", "title"],
}, {
  cache: {
    enable: true,
    key: async (args, cachingModuleService) => {
      return await cachingModuleService.computeKey({
        ...args,
        prefix: "products",
      })
    },
  },
})
```

In the example above, you pass a function to `key`. It accepts two parameters:

1. The arguments of `query.index` passed as an array.
2. The [Caching Module's service](https://docs.medusajs.com/resources/references/caching-service/index.html.md).

You generate the key using the [computeKey method of the Caching Module's service](https://docs.medusajs.com/resources/references/caching-service#computeKey/index.html.md). The query results will be cached with that key.

### Set Cache Tags

By default, the Caching Module generates relevant tags for a query based on the entity and its retrieved relations. Cache tags are useful to group related items together, allowing you to [retrieve](https://docs.medusajs.com/resources/references/caching-service#get/index.html.md) or [invalidate](https://docs.medusajs.com/resources/references/caching-service#clear/index.html.md) items by common tags.

Alternatively, you can set the cache tags of a query manually. This is useful if you want to manage invalidating the cache manually, or you want to group related cached items with custom tags.

To set the cache tags of a query, pass the `cache.tags` option:

```ts highlights={[["7"]]}
const { data: products } = await query.index({
  entity: "product",
  fields: ["id", "title"],
}, {
  cache: {
    enable: true,
    tags: ["Product:list:*"],
  },
})
```

In the example above, you cache the query results with the `Product:list:*` tag.

The cache tag must follow the [Caching Tags Convention](https://docs.medusajs.com/resources/infrastructure-modules/caching/concepts#caching-tags-convention/index.html.md) to be automatically invalidated.

You can also pass a function as the value of `cache.tags`:

```ts highlights={[["7"], ["8"], ["9"], ["10"], ["11"], ["12"], ["13"]]}
const { data: products } = await query.index({
  entity: "product",
  fields: ["id", "title"],
}, {
  cache: {
    enable: true,
    tags: (args) => {
      const collectionId = args[0].filter?.collection_id
      return [
        ...args,
        collectionId ? `ProductCollection:${collectionId}` : undefined,
      ]
    },
  },
})
```

In the example above, you use a function to determine the cache tags. The function accepts the arguments passed to `query.index` as an array.

Then, you add the `ProductCollection:id` tag if `collection_id` is passed in the query filters.

### Set TTL

By default, the Caching Module will pass the [configured time-to-live (TTL)](https://docs.medusajs.com/resources/infrastructure-modules/caching#caching-module-options/index.html.md) to the Caching Module Provider when caching data. The Caching Module Provider may also have its own default TTL.  The cache isn't invalidated until the configured TTL passes.

Alternatively, you can set a custom TTL for a query. This is useful if you want the cached data to be invalidated sooner or later than the default TTL.

To set the TTL of the cached query results to a custom value, use the `cache.ttl` option:

```ts highlights={[["7"]]}
const { data: products } = await query.index({
  entity: "product",
  fields: ["id", "title"],
}, {
  cache: {
    enable: true,
    ttl: 100, // 100 seconds
  },
})
```

In the example above, you set the TTL of the cached query result to `100` seconds. It will be invalidated after that time.

You can also pass a function as the value of `cache.ttl`:

```ts highlights={[["10"], ["11"], ["12"]]}
const { data: products } = await query.index({
  entity: "product",
  fields: ["id", "title"],
  filters: {
    id: "prod_123",
  },
}, {
  cache: {
    enable: true,
    ttl: (args) => {
      return args[0].filters.id === "test" ? 10 : 100
    },
  },
})
```

In the example above, you use a function to determine the TTL. The function accepts the arguments passed to `query.index` as an array.

Then, you set the TTL based on the ID of the product passed in the filters.

### Set Auto Invalidation

By default, the Caching Module automatically invalidates cached query results when the data changes.

Alternatively, you can disable auto invalidation of cached query results. This is useful if you want to manage invalidating the cache manually.

To configure invalidation behavior, use the `cache.autoInvalidate` option:

```ts highlights={[["7"]]}
const { data: products } = await query.index({
  entity: "product",
  fields: ["id", "title"],
}, {
  cache: {
    enable: true,
    autoInvalidate: false,
  },
})
```

In this example, you disable auto invalidation of the query result. You must [invalidate](https://docs.medusajs.com/resources/references/caching-service#clear/index.html.md) the cached data manually.

You can also pass a function as the value of `cache.autoInvalidate`:

```ts highlights={[["7"], ["8"], ["9"]]}
const { data: products } = await query.index({
  entity: "product",
  fields: ["id", "title"],
}, {
  cache: {
    enable: true,
    autoInvalidate: (args) => {
      return !args[0].fields.includes("custom_field")
    },
  },
})
```

In the example above, you use a function to determine whether to invalidate the cached query result automatically. The function accepts the arguments passed to `query.index` as an array.

Then, you enable auto-invalidation only if the `fields` passed to `query.index` don't include `custom_fields`. If this disables auto-invalidation, you must [invalidate](https://docs.medusajs.com/resources/references/caching-service#clear/index.html.md) the cached data manually.

Learn more about automatic invalidation in the [Caching Module documentation](https://docs.medusajs.com/resources/infrastructure-modules/caching/concepts#automatic-cache-invalidation/index.html.md).

### Set Caching Provider

By default, the Caching Module uses the [default Caching Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/caching/providers#default-caching-module-provider/index.html.md) to cache a query.

Alternatively, you can set the caching provider to use for a query. This is useful if you have multiple caching providers configured, and you want to use a specific one for a query, or you want to specify a fallback provider.

To configure the caching providers, use the `cache.providers` option:

```ts highlights={[["7"]]}
const { data: products } = await query.index({
  entity: "product",
  fields: ["id", "title"],
}, {
  cache: {
    enable: true,
    providers: ["caching-redis", "caching-memcached"],
  },
})
```

In the example above, you specify the providers with ID `caching-redis` and `caching-memcached` to cache the query results. These IDs must match the IDs of the providers in `medusa-config.ts`.

When you pass multiple providers, the cache is stored and retrieved in those providers in order.

You can also pass a function as the value of `cache.providers`:

```ts highlights={[["10"], ["11"], ["12"]]}
const { data: products } = await query.index({
  entity: "product",
  fields: ["id", "title"],
  filters: {
    id: "prod_123",
  },
}, {
  cache: {
    enable: true,
    providers: (args) => {
      return args[0].filters.id === "test" ? ["caching-redis"] : ["caching-memcached"]
    },
  },
})
```

In the example above, you use a function to determine the caching providers. The function accepts the arguments passed to `query.index` as an array.

Then, you set the providers based on the ID of the product passed in the filters.

***

## index Method Usage Examples

The following sections show examples of how to use the `index` method in different scenarios.

### Retrieve Linked Data Models

Retrieve the records of a linked data model by passing in fields the data model's name suffixed with `.*`.

For example:

```ts title="src/api/custom/products/route.ts" highlights={[["3"]]}
const { data: products } = await query.index({
  entity: "product",
  fields: ["*", "brand.*"],
})
```

This will return all products with their linked brand data model.

### Use Advanced Filters

When setting filters on properties, you can use advanced filters like `$ne` and `$gt`. These are the same advanced filters accepted by the [listing methods generated by the Service Factory](https://docs.medusajs.com/resources/service-factory-reference/tips/filtering/index.html.md).

For example, to only retrieve products linked to a brand:

```ts title="src/api/custom/products/route.ts" highlights={[["9"]]}
const { 
  data: products,
} = await query.index({
  entity: "product",
  fields: ["*", "brand.*"],
  filters: {
    brand: {
      id: {
        $ne: null,
      },
    },
  },
})
```

You use the `$ne` operator to filter products that are linked to a brand.

Another example is to retrieve products whose brand name starts with `Acme`:

```ts title="src/api/custom/products/route.ts" highlights={[["9"]]}
const { 
  data: products,
} = await query.index({
  entity: "product",
  fields: ["*", "brand.*"],
  filters: {
    brand: {
      name: {
        $like: "Acme%",
      },
    },
  },
})
```

This will return all products whose brand name starts with `Acme`.

### Use Request Query Configurations

API routes using the `graph` method can configure default [query configurations](https://docs.medusajs.com/learn/fundamentals/module-links/query#request-query-configurations/index.html.md), such as which fields to retrieve, while also allowing clients to override them using query parameters.

The `index` method supports the same configurations. For example, if you add the request query configuration as explained in the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#request-query-configurations/index.html.md), you can use those configurations in the `index` method:

```ts title="src/api/custom/products/route.ts" highlights={[["17"]]}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve("query")
  
  const { 
    data: products,
    metadata,
  } = await query.index({
    entity: "product",
    ...req.queryConfig,
    filters: {
      brand: {
        name: "Acme",
      },
    },
  })

  res.json({ products, ...metadata })
}
```

You pass the `req.queryConfig` object to the `index` method, which will contain the fields and pagination properties to use in the query.

### Use Index Module in Workflows

In a workflow's step, you can resolve `query` and use its `index` method to retrieve data using the Index Module.

For example:

```ts title="src/workflows/custom-workflow.ts" highlights={workflowHighlights}
import {
  createStep,
  createWorkflow,
  StepResponse,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

const retrieveBrandsStep = createStep(
  "retrieve-brands",
  async ({}, { container }) => {
    const query = container.resolve("query")

    const { data: brands } = await query.index({
      entity: "brand",
      fields: ["*", "products.*"],
      filters: {
        products: {
          id: {
            $ne: null,
          },
        },
      },
    })

    return new StepResponse(brands)
  }
)

export const retrieveBrandsWorkflow = createWorkflow(
  "retrieve-brands",
  () => {
    const retrieveBrands = retrieveBrandsStep()

    return new WorkflowResponse(retrieveBrands)
  }
)
```

This will retrieve all brands that are linked to at least one product.

### Retrieve Localized Data

### Prerequisites

- [Medusa v2.12.4 or later](https://github.com/medusajs/medusa/releases/tag/v2.12.4)
- [Translation Module Configured](https://docs.medusajs.com/resources/commerce-modules/translation#configure-translation-module/index.html.md)

To retrieve localized data for data models that have translations, pass a `locale` property in the second parameter object of the `query.index` method.

```ts highlights={[["7", "locale", "Pass the locale to retrieve localized data."]]}
const { data: products } = await query.index(
  {
    entity: "product",
    fields: ["id", "title", "description"],
  },
  {
    locale: "fr-FR",
  }
)
```

The `locale` property is a string representing the locale code following the [IETF BCP 47 standard](https://gist.github.com/typpo/b2b828a35e683b9bf8db91b5404f1bd1).

The returned products will have their `title` and `description` properties in French (`fr-FR`), if translations are available.

Learn more in the [Translation Module](https://docs.medusajs.com/resources/commerce-modules/translation/index.html.md) documentation.


# Link

In this chapter, you’ll learn what Link is and how to use it to manage links.

As of [Medusa v2.2.0](https://github.com/medusajs/medusa/releases/tag/v2.2.0), Remote Link has been deprecated in favor of Link. They have the same usage, so you only need to change the key used to resolve the tool from the Medusa container as explained below.

## What is Link?

Link is a class with utility methods to manage links between data models. It’s registered in the Medusa container under the `link` registration name.

For example:

```ts collapsibleLines="1-9" expandButtonLabel="Show Imports"
import { 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { 
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export async function POST(
  req: MedusaRequest,
  res: MedusaResponse
): Promise<void> {
  const link = req.scope.resolve(
    ContainerRegistrationKeys.LINK
  )
    
  // ...
}
```

You can use its methods to manage links, such as create or delete links.

### Link Usage in Workflows

To perform link operations in a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), Medusa provides the following steps out-of-the-box:

- [createRemoteLinkStep](https://docs.medusajs.com/resources/references/helper-steps/createRemoteLinkStep/index.html.md): Creates a link between records of two data models.
- [dismissRemoteLinkStep](https://docs.medusajs.com/resources/references/helper-steps/dismissRemoteLinkStep/index.html.md): Removes a link between records of two data models.
- [removeRemoteLinkStep](https://docs.medusajs.com/resources/references/helper-steps/removeRemoteLinkStep/index.html.md): Deletes all linked records whose links are defined with cascade deletion.
- [updateRemoteLinksStep](https://docs.medusajs.com/resources/references/helper-steps/updateRemoteLinksStep/index.html.md): Updates links between records of two data models.

The rest of this chapter provides examples using the Link class and the workflow steps.

***

## Create Link

To create a link between records of two data models, use the `create` method of Link.

For example:

### Link

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  blog: {
    post_id: "post_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
import { 
  createWorkflow,
  transform,
} from "@medusajs/framework/workflows-sdk"

export const myWorkflow = createWorkflow(
  "my-workflow",
  () => {
    // ...
    const linkData = transform({
      // TODO pass input data
    }, () => {
      return [
        {
          [Modules.PRODUCT]: {
            product_id: "prod_123",
          },
          blog: {
            post_id: "post_123",
          },
        },
      ]
    })
    createRemoteLinkStep(linkData)
    // ...
  }
)
```

The `create` method accepts as a parameter an object. The object’s keys are the names of the linked modules.

The keys (names of linked modules) must be in the same [direction](https://docs.medusajs.com/learn/fundamentals/module-links/directions/index.html.md) of the link definition.

The value of each module’s property is an object, whose keys are of the format `{data_model_snake_name}_id`, and values are the IDs of the linked record.

So, in the example above, you link a record of the `Post` data model in a `blog` module to a `Product` record in the Product Module.

### Enforced Integrity Constraints on Link Creation

Medusa enforces integrity constraints on links based on the link's relation type. So, an error is thrown in the following scenarios:

- If the link is one-to-one and one of the linked records already has a link to another record of the same data model. For example:

```ts
// no error
await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  blog: {
    post_id: "post_123",
  },
})

// throws an error because `prod_123` already has a link to `post_123`
await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  blog: {
    post_id: "mc_456",
  },
})
```

- If the link is one-to-many and the "one" side already has a link to another record of the same data model. For example, if a product can have many `Post` records, but a `Post` record can only have one product:

```ts
// no error
await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  blog: {
    post_id: "post_123",
  },
})

// also no error
await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  blog: {
    post_id: "mc_456",
  },
})

// throws an error because `post_123` already has a link to `prod_123`
await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_456",
  },
  blog: {
    post_id: "post_123",
  },
})
```

There are no integrity constraints in a many-to-many link, so you can create multiple links between the same records.

***

## Dismiss Link

To remove a link between records of two data models, use the `dismiss` method of Link.

For example:

### Link

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.dismiss({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  blog: {
    post_id: "post_123",
  },
})
```

### dismissRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { dismissRemoteLinkStep } from "@medusajs/medusa/core-flows"
import { 
  createWorkflow,
  transform,
} from "@medusajs/framework/workflows-sdk"

export const myWorkflow = createWorkflow(
  "my-workflow",
  () => {
    // ...
    const linkData = transform({
      // TODO pass input data
    }, () => {
      return [
        {
          [Modules.PRODUCT]: {
            product_id: "prod_123",
          },
          blog: {
            post_id: "post_123",
          },
        },
      ]
    })
    dismissRemoteLinkStep(linkData)
    // ...
  }
)
```

The `dismiss` method accepts the same parameter type as the [create method](#create-link).

The keys (names of linked modules) must be in the same [direction](https://docs.medusajs.com/learn/fundamentals/module-links/directions/index.html.md) of the link definition.

***

## Cascade Delete Linked Records

If you delete a record using a workflow or its module's service, use the `delete` method of Link to delete all linked records whose links are defined with [cascade deletion](https://docs.medusajs.com/learn/fundamentals/module-links#set-delete-cascades-on-link/index.html.md).

For example:

### Link

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await productModuleService.deleteProducts(["prod_123"])

await link.delete({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
})
```

### removeRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { removeRemoteLinkStep } from "@medusajs/medusa/core-flows"
import { 
  createWorkflow,
  transform,
} from "@medusajs/framework/workflows-sdk"

export const myWorkflow = createWorkflow(
  "my-workflow",
  () => {
    // ...
    const linkData = transform({
      // TODO pass input data
    }, () => {
      return [
        {
          [Modules.PRODUCT]: {
            product_id: "prod_123",
          },
        },
      ]
    })
    removeRemoteLinkStep(linkData)
    // ...
  }
)
```

This deletes all records linked to the deleted product.

***

## Restore Linked Records

If a record that was previously soft-deleted is now restored, use the `restore` method of Link to restore all linked records.

For example:

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await productModuleService.restoreProducts(["prod_123"])

await link.restore({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
})
```

***

## Update Links

Links may have [custom columns](https://docs.medusajs.com/learn/fundamentals/module-links/custom-columns/index.html.md) to store additional information, such as a `metadata` column to store JSON data about the link.

You can update the custom columns of existing links by calling the `create` method again with the same linked records and the new values for the custom columns.

For example:

### Link

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  blog: {
    post_id: "post_123",
  },
  data: {
    metadata: {
      featured: true,
    },
  },
})
```

### updateRemoteLinksStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { updateRemoteLinksStep } from "@medusajs/medusa/core-flows"
import { 
  createWorkflow,
  transform,
} from "@medusajs/framework/workflows-sdk"

export const myWorkflow = createWorkflow(
  "my-workflow",
  () => {
    // ...
    const linkData = transform({
      // TODO pass input data
    }, () => {
      return [
        {
          [Modules.PRODUCT]: {
            product_id: "prod_123",
          },
          blog: {
            post_id: "post_123",
          },
          data: {
            metadata: {
              featured: true,
            },
          },
        },
      ]
    })
    updateRemoteLinksStep(linkData)
    // ...
  }
)
```

The object parameter you pass to the `create` method (or the `updateRemoteLinksStep` step) accepts an optional `data` property. The value of this property is an object whose keys are the names of the custom columns to update, and values are the new values for those columns.

Learn more in the [Custom Columns](https://docs.medusajs.com/learn/fundamentals/module-links/custom-columns/index.html.md) chapter.


# Define Module Link

In this chapter, you’ll learn what a module link is and how to define one.

## What is a Module Link?

Medusa's modular architecture isolates modules from one another to ensure they can be integrated into your application without side effects. Module isolation has other benefits, which you can learn about in the [Module Isolation chapter](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md). Since modules are isolated, you can't access another module's data models to add a relation to it or extend it. Instead, you use a module link.

A module link forms an association between two data models of different modules while maintaining module isolation. Using module links, you can build virtual relations between your custom data models and data models in the Commerce Modules, which is useful as you extend the features provided by the Commerce Modules. Then, Medusa creates a link table in the database to store the IDs of the linked records. You'll learn more about link tables later in this chapter.

For example, the [Brand Customizations Tutorial](https://docs.medusajs.com/learn/customization/extend-features/index.html.md) shows how to create a Brand Module that adds the concept of brands to your application, then link those brands to a product.

***

## How to Define a Module Link?

### 1. Create Link File

Module links are defined in a TypeScript or JavaScript file under the `src/links` directory. The file defines the link using `defineLink` from the Modules SDK and exports it.

For example:

```ts title="src/links/blog-product.ts" highlights={highlights}
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  ProductModule.linkable.product,
  BlogModule.linkable.post
)
```

The `defineLink` function accepts as parameters the link configurations of each module's data model. A module has a special `linkable` property that holds these configurations for its data models.

In this example, you define a module link between the `blog` module's `post` data model and the Product Module's `Product` data model.

### 2. Sync Links

After defining the link, run the `db:sync-links` command:

```bash
npx medusa db:sync-links
```

The Medusa application creates a new table for your module link to store the IDs of linked records.

You can also use the `db:migrate` command, which runs both the migrations and syncs the links.

Use either of these commands whenever you make changes to your link definitions. For example, run this command if you remove your link definition file.

***

### Module Link's Database Table

When you define a module link, the Medusa application creates a table in the database for that module link. The table's name is a combination of the names of the two data models linked in the format `module1_table1_module2_table2`, where:

- `module1` and `module2` are the names of the modules.
- `table1` and `table2` are the table names of the data models.

For example, if you define a link between the `Product` data model from the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) and a `Post` data model from a Blog Module, the table name would be `product_product_blog_post`.

The table has two columns, each storing the ID of a record from the linked data models. For example, the `product_product_blog_post` table would have columns `product_id` and `post_id`. These columns store only the IDs of the linked records and do not hold a foreign key constraint.

Then, when you create links between records of the data models, the IDs of these data models are stored as a new record in the link's table.

You can also add custom columns in the link table as explained in the [Add Columns to Link Table chapter](https://docs.medusajs.com/learn/fundamentals/module-links/custom-columns/index.html.md).

![Diagram illustration for module links](https://res.cloudinary.com/dza7lstvk/image/upload/v1741696766/Medusa%20Book/custom-links_vezsx8.jpg)

***

## When to Use Module Links

- You want to create a relation between data models from different modules.
- You want to extend the data model of another module.

You want to create a relationship between data models in the same module. Use data model relationships instead.

***

## Define a List Module Link

By default, a module link establishes a one-to-one relation: a record of a data model is linked to one record of the other data model.

To specify that a data model can have multiple of its records linked to the other data model's record, use the `isList` option.

For example:

```ts
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  ProductModule.linkable.product,
  {
    linkable: BlogModule.linkable.post,
    isList: true,
  }
)
```

In this case, you pass an object of configuration as a parameter instead. The object accepts the following properties:

- `linkable`: The data model's link configuration.
- `isList`: Whether multiple records can be linked to one record of the other data model.

In this example, a record of `product` can be linked to more than one record of `post`.

### Many-to-Many Module Link

Your module link can also establish a many-to-many relation between the linked data models. To do this, enable `isList` on both sides of the link.

For example:

```ts
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    isList: true,
  },
  {
    linkable: BlogModule.linkable.post,
    isList: true,
  }
)
```

***

## Set Delete Cascades on Link

To enable delete cascade on a link so that when a record is deleted with [link.delete](https://docs.medusajs.com/learn/fundamentals/module-links/link#cascade-delete-linked-records/index.html.md), its linked records are also deleted, pass the `deleteCascade` property in the object passed to `defineLink`.

For example:

```ts
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  ProductModule.linkable.product,
  {
    linkable: BlogModule.linkable.post,
    deleteCascade: true,
  }
)
```

In this example, when a product is deleted with [link.delete](https://docs.medusajs.com/learn/fundamentals/module-links/link#cascade-delete-linked-records/index.html.md), all linked blog posts are also deleted.

***

## Renaming Participants in a Module Link

As mentioned in the [Module Link's Database Table](#module-links-database-table) section, the name of a link's table consists of the names of the modules and the data models' table names.

So, if you rename a module or a data model's table, then run the `db:sync-links` or `db:migrate` commands, you'll be asked to delete the old link table and create a new one.

A data model's table name is passed in the first parameter of `model.define`, and a module's name is passed in the first parameter of `Module` in the module's `index.ts` file.

For example, if you have the link table `product_product_blog_post` and you rename the Blog Module from `blog` to `article`, Medusa considers the old link definition deleted. Then, when you run the `db:sync-links` or `db:migrate` command, Medusa will ask if you want to delete the old link table, and will create a new one with the new name `product_product_article_post`.

To resolve this, you can rename the link table in the link definition.

### Rename Link Table

If you need to rename a module or its data model's table, you can persist the old name by passing a third parameter to `defineLink`. This parameter is an object of additional configurations. It accepts a `database` property that allows you to configure the link's table name.

For example, after renaming the Blog Module to `article`, you can persist the old name `blog` in the link table name:

```ts highlights={renameHighlights}
import ArticleModule from "../modules/article"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  ProductModule.linkable.product,
  {
    linkable: ArticleModule.linkable.post,
    isList: true,
  },
  {
    database: {
      table: "product_product_blog_post",
    },
  }
)
```

In this example, you set the `table` property in the `database` object to the old link table name `product_product_blog_post`, ensuring that the old link table is not deleted.

This is enough to rename the link table when you rename a module. If you renamed a data model's table, you need to also run the `db:sync-links` or `db:migrate` commands, which will update the column names in the link table automatically:

```bash
npx medusa db:migrate
```

***

## Delete Module Link Definition

To delete a module link definition, remove the link file from the `src/links` directory. Then, run the `db:sync-links` or `db:migrate` command to delete the link table from the database:

```bash
npx medusa db:migrate
```


# Query Context

In this chapter, you'll learn how to pass context when retrieving data with [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md).

## What is Query Context?

Query context is additional information you pass to Query when retrieving data. You can use it to apply custom transformations to the returned data based on the current context.

For example, consider a Blog Module with posts and authors. You can send the user's language as context to Query to retrieve posts in the user's language.

Medusa also uses Query Context to [retrieve product variants' prices based on the customer's currency](https://docs.medusajs.com/resources/commerce-modules/product/guides/price/index.html.md).

***

## How to Pass Query Context

The `query.graph` method accepts an optional `context` parameter. You can use this to pass additional context to the data model you're retrieving (for example, `post`) or its related and linked models (for example, `author`).

You can initialize a context using `QueryContext` from the Modules SDK. It accepts an object of contexts as an argument.

For example, to retrieve posts using Query while passing the user's language as context:

```ts highlights={highlights1}
import { QueryContext } from "@medusajs/framework/utils"

// ...

const { data } = await query.graph({
  entity: "post",
  fields: ["*"],
  context: QueryContext({
    lang: "es",
  }),
})
```

In this example, you pass a `lang` property with the value `es` in the context. You create the context using `QueryContext`.

### How to Handle Query Context

To handle the Query context passed while retrieving records of your data models, override the [generated list method](https://docs.medusajs.com/resources/service-factory-reference/methods/list/index.html.md) of the associated module's service.

For example, continuing the example above, you can override the `listPosts` method of the Blog Module's service to handle the Query context:

```ts highlights={highlights2}
import { MedusaContext, MedusaService } from "@medusajs/framework/utils"
import { Context, FindConfig } from "@medusajs/framework/types"
import Post from "./models/post"
import Author from "./models/author"

class BlogModuleService extends MedusaService({
  Post,
  Author,
}){
  // @ts-ignore
  async listPosts(
    filters?: any, 
    config?: FindConfig<any> | undefined, 
    @MedusaContext() sharedContext?: Context | undefined
  ) {
    const context = filters.context ?? {}
    delete filters.context

    let posts = await super.listPosts(filters, config, sharedContext)

    if (context.lang === "es") {
      posts = posts.map((post) => {
        return {
          ...post,
          title: post.title + " en español",
        }
      })
    }

    return posts
  }
}

export default BlogModuleService
```

In the above example, you override the generated `listPosts` method. This method receives the filters passed to `query.graph` as its first parameter. The first parameter includes a `context` property that holds the Query context alsopassed to `query.graph`.

You extract the context from `filters`, then retrieve the posts using the parent's `listPosts` method. If the language is set in the context, you transform the post titles.

All posts returned will now have their titles appended with "en español".

### Using Pagination with Query

If you pass pagination fields to `query.graph`, you must also override the [generated listAndCount method](https://docs.medusajs.com/resources/service-factory-reference/methods/listAndCount/index.html.md) in the service.

For example, following the previous example, you must override the `listAndCountPosts` method of the Blog Module's service:

```ts
import { MedusaContext, MedusaService } from "@medusajs/framework/utils"
import { Context, FindConfig } from "@medusajs/framework/types"
import Post from "./models/post"
import Author from "./models/author"

class BlogModuleService extends MedusaService({
  Post,
  Author,
}){
  // @ts-ignore
  async listAndCountPosts(
    filters?: any, 
    config?: FindConfig<any> | undefined, 
    @MedusaContext() sharedContext?: Context | undefined
  ) {
    const context = filters.context ?? {}
    delete filters.context

    const result = await super.listAndCountPosts(
      filters, 
      config, 
      sharedContext
    )

    if (context.lang === "es") {
      result[0] = result[0].map((post) => {
        return {
          ...post,
          title: post.title + " en español",
        }
      })
    }

    return result
  }
}

export default BlogModuleService
```

Now, the `listAndCountPosts` method will handle the context passed to `query.graph` when you pass pagination fields. You can also move the logic to transform the post titles to a separate method and call it from both `listPosts` and `listAndCountPosts`.

***

## Passing Query Context to Related Data Models

If you're retrieving a data model and want to pass Query context to its associated model in the same module, pass them as part of `QueryContext`'s parameter. Then, you can handle them in the same `list` method.

To pass Query context to linked data models, check out the [next section](#passing-query-context-to-linked-data-models).

For example, to pass a context for the post's authors:

```ts highlights={highlights3}
const { data } = await query.graph({
  entity: "post",
  fields: ["*"],
  context: QueryContext({
    lang: "es",
    author: QueryContext({
      lang: "es",
    }),
  }),
})
```

Then, in the `listPosts` method, you can handle the context for the post's authors:

```ts highlights={highlights4}
import { MedusaContext, MedusaService } from "@medusajs/framework/utils"
import { Context, FindConfig } from "@medusajs/framework/types"
import Post from "./models/post"
import Author from "./models/author"

class BlogModuleService extends MedusaService({
  Post,
  Author,
}){
  // @ts-ignore
  async listPosts(
    filters?: any, 
    config?: FindConfig<any> | undefined, 
    @MedusaContext() sharedContext?: Context | undefined
  ) {
    const context = filters.context ?? {}
    delete filters.context

    let posts = await super.listPosts(filters, config, sharedContext)

    const isPostLangEs = context.lang === "es"
    const isAuthorLangEs = context.author?.lang === "es"

    if (isPostLangEs || isAuthorLangEs) {
      posts = posts.map((post) => {
        return {
          ...post,
          title: isPostLangEs ? post.title + " en español" : post.title,
          author: {
            ...post.author,
            name: isAuthorLangEs ? post.author.name + " en español" : post.author.name,
          },
        }
      })
    }

    return posts
  }
}

export default BlogModuleService
```

The context in `filters` will also include the context for `author`, which you can use to transform the post's authors.

***

## Passing Query Context to Linked Data Models

If you're retrieving a data model and want to pass Query context to a linked model in a different module, pass an object to the `context` property instead. The object's keys should be the linked model's name, and the values should be the Query context for that linked model.

For example, consider the Product Module's `Product` data model is linked to the Blog Module's `Post` data model. You can pass context to the `Post` data model while retrieving products:

```ts highlights={highlights5}
const { data } = await query.graph({
  entity: "product",
  fields: ["*", "post.*"],
  context: {
    post: QueryContext({
      lang: "es",
    }),
  },
})
```

In this example, you retrieve products and their associated posts. You also pass Query context for `post`, indicating the customer's language.

To handle the context, override the generated `listPosts` method of the Blog Module as explained [previously](#how-to-handle-query-context).


# Query

In this chapter, you’ll learn about Query and how to use it to fetch data from modules.

## What is Query?

Query fetches data across modules. It’s a set of methods registered in the Medusa container under the `query` key.

In all resources that can access the [Medusa Container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md), such as API routes or workflows, you can resolve Query to fetch data across custom modules and Medusa’s Commerce Modules.

***

## Query Example

For example, create the route `src/api/query/route.ts` with the following content:

```ts title="src/api/query/route.ts" highlights={exampleHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports"
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import {
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: posts } = await query.graph({
    entity: "post",
    fields: ["id", "title"],
  })

  res.json({ posts })
}
```

In the above example, you resolve Query from the Medusa container using the `ContainerRegistrationKeys.QUERY` (`query`) key.

Then, you run a query using its `graph` method. This method accepts as a parameter an object with the following required properties:

- `entity`: The data model's name, as specified in the first parameter of the `model.define` method used for the data model's definition.
- `fields`: An array of the data model’s properties to retrieve in the result.

The method returns an object that has a `data` property, which holds an array of the retrieved data. For example:

```json title="Returned Data"
{
  "data": [
    {
      "id": "123",
      "title": "My Post"
    }
  ]
}
```

### Query Usage in Workflows

To retrieve data with Query in a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), use the [useQueryGraphStep](https://docs.medusajs.com/resources/references/helper-steps/useQueryGraphStep/index.html.md).

For example:

```ts title="src/workflows/query.ts"
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

const myWorkflow = createWorkflow(
  "my-workflow",
  () => {
    const { data: posts } = useQueryGraphStep({
      entity: "post",
      fields: ["id", "title"],
    })

    return new WorkflowResponse({
      posts,
    })
  }
)
```

You can learn more about this step in the [useQueryGraphStep](https://docs.medusajs.com/resources/references/helper-steps/useQueryGraphStep/index.html.md) reference.

***

## Querying the Graph

When you use the `query.graph` method, you're running a query through an internal graph that the Medusa application creates.

This graph collects data models of all modules in your application, including commerce and custom modules, and identifies relations and links between them.

***

## Retrieve Linked Records

Retrieve the records of a linked data model by passing in `fields` the data model's name suffixed with `.*`.

For example:

### query.graph

```ts highlights={[["6"]]}
const { data: posts } = await query.graph({
  entity: "post",
  fields: [
    "id", 
    "title",
    "product.*",
  ],
})
```

### useQueryGraphStep

```ts highlights={[["6"]]}
const { data: posts } = useQueryGraphStep({
  entity: "post",
  fields: [
    "id", 
    "title",
    "product.*",
  ],
})
```

`.*` means that all of the data model's properties should be retrieved. You can also retrieve specific properties by replacing the `*` with the property name for each property.

For example:

### query.graph

```ts
const { data: posts } = await query.graph({
  entity: "post",
  fields: [
    "id", 
    "title",
    "product.id",
    "product.title",
  ],
})
```

### useQueryGraphStep

```ts
const { data: posts } = useQueryGraphStep({
  entity: "post",
  fields: [
    "id", 
    "title",
    "product.id",
    "product.title",
  ],
})
```

In the example above, you retrieve only the `id` and `title` properties of the `product` linked to a `post`.

### Retrieve List Link Records

If the linked data model has `isList` enabled in the link definition, pass in `fields` the data model's plural name suffixed with `.*`.

For example:

### query.graph

```ts highlights={[["6"]]}
const { data: posts } = await query.graph({
  entity: "post",
  fields: [
    "id", 
    "title",
    "products.*",
  ],
})
```

### useQueryGraphStep

```ts highlights={[["6"]]}
const { data: posts } = useQueryGraphStep({
  entity: "post",
  fields: [
    "id", 
    "title",
    "products.*",
  ],
})
```

In the example above, you retrieve all products linked to a post.

### Apply Filters and Pagination on Linked Records

Consider that you want to apply filters or pagination configurations on the product(s) linked to a `post`. To do that, you must query the module link's table instead.

As mentioned in the [Module Link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) documentation, Medusa creates a table for your module link. So, not only can you retrieve linked records, but you can also retrieve the records in a module link's table.

A module link's definition, exported by a file under `src/links`, has a special `entryPoint` property. Use this property when specifying the `entity` property in Query's `graph` method.

For example:

```ts highlights={queryLinkTableHighlights}
import ProductPostLink from "../../../links/product-post"

// ...

const { data: productCustoms } = await query.graph({
  entity: ProductPostLink.entryPoint,
  fields: ["*", "product.*", "post.*"],
  pagination: {
    take: 5,
    skip: 0,
  },
})
```

In the object passed to the `graph` method:

- You pass the `entryPoint` property of the link definition as the value for `entity`. So, Query will retrieve records from the module link's table.
- You pass three items to the `fields` property:
  - `*` to retrieve the link table's fields. This is useful if the link table has [custom columns](https://docs.medusajs.com/learn/fundamentals/module-links/custom-columns/index.html.md).
  - `product.*` to retrieve the fields of a product record linked to a `Post` record.
  - `post.*` to retrieve the fields of a `Post` record linked to a product record.

You can then apply any [filters](#apply-filters) or [pagination configurations](#apply-pagination) on the module link's table. For example, you can apply filters on the `product_id`, `post_id`, and any other custom columns you defined in the link table.

The returned `data` is similar to the following:

```json title="Example Result"
[{
  "id": "123",
  "product_id": "prod_123",
  "post_id": "123",
  "product": {
    "id": "prod_123",
    // other product fields...
  },
  "post": {
    "id": "123",
    // other post fields...
  }
}]
```

***

## Apply Filters

### query.graph

```ts highlights={[["4"], ["5"], ["6"]]}
const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    id: "post_123",
  },
})
```

### useQueryGraphStep

```ts highlights={[["4"], ["5"], ["6"]]}
const { data: posts } = useQueryGraphStep({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    id: "post_123",
  },
})
```

The `query.graph` function accepts a `filters` property. You can use this property to filter retrieved records.

In the example above, you filter the `post` records by the ID `post_123`.

You can also filter by multiple values of a property. For example:

### query.graph

```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"], ["9"]]}
const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    id: [
      "post_123",
      "post_321",
    ],
  },
})
```

### useQueryGraphStep

```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"], ["9"]]}
const { data: posts } = useQueryGraphStep({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    id: [
      "post_123",
      "post_321",
    ],
  },
})
```

In the example above, you filter the `post` records by multiple IDs.

Filters don't apply on fields of linked data models from other modules. Refer to the [Retrieve Linked Records](#retrieve-linked-records) section for an alternative solution.

### Advanced Query Filters

Under the hood, Query uses one of the following methods from the data model's module's service to retrieve records:

- `listX` if you don't pass [pagination parameters](#apply-pagination). For example, `listPosts`.
- `listAndCountX` if you pass pagination parameters. For example, `listAndCountPosts`.

Both methods accept a filter object that can be used to filter records.

Those filters don't just allow you to filter by exact values. You can also filter by properties that don't match a value, match multiple values, and other filter types.

Refer to the [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/tips/filtering/index.html.md) for examples of advanced filters. The following sections provide some quick examples.

#### Filter by Not Matching a Value

### query.graph

```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    title: {
      $ne: null,
    },
  },
})
```

### useQueryGraphStep

```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
const { data: posts } = useQueryGraphStep({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    title: {
      $ne: null,
    },
  },
})
```

In the example above, only posts that have a title are retrieved.

#### Filter by Not Matching Multiple Values

### query.graph

```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    title: {
      $nin: ["My Post", "Another Post"],
    },
  },
})
```

### useQueryGraphStep

```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
const { data: posts } = useQueryGraphStep({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    title: {
      $nin: ["My Post", "Another Post"],
    },
  },
})
```

In the example above, only posts that don't have the title `My Post` or `Another Post` are retrieved.

#### Filter by a Range

### query.graph

```ts highlights={[["10"], ["11"], ["12"], ["13"], ["14"], ["15"]]}
const startToday = new Date()
startToday.setHours(0, 0, 0, 0)

const endToday = new Date()
endToday.setHours(23, 59, 59, 59)

const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    published_at: {
      $gt: startToday,
      $lt: endToday,
    },
  },
})
```

### useQueryGraphStep

```ts highlights={[["10"], ["11"], ["12"], ["13"], ["14"], ["15"]]}
const startToday = new Date()
startToday.setHours(0, 0, 0, 0)

const endToday = new Date()
endToday.setHours(23, 59, 59, 59)

const { data: posts } = useQueryGraphStep({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    published_at: {
      $gt: startToday,
      $lt: endToday,
    },
  },
})
```

In the example above, only posts that were published today are retrieved.

#### Filter Text by Like Value

This filter only applies to text-like properties, including `text`, `id`, and `enum` properties.

### query.graph

```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    title: {
      $like: "%My%",
    },
  },
})
```

### useQueryGraphStep

```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
const { data: posts } = useQueryGraphStep({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    title: {
      $like: "%My%",
    },
  },
})
```

In the example above, only posts that have the word `My` in their title are retrieved.

#### Filter a Relation's Property

### query.graph

```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    author: {
      name: "John",
    },
  },
})
```

### useQueryGraphStep

```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
const { data: posts } = useQueryGraphStep({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    author: {
      name: "John",
    },
  },
})
```

While it's not possible to filter by a linked data model's property, you can filter by a relation's property (that is, the property of a related data model that is defined in the same module).

In the example above, only posts that have an author with the name `John` are retrieved.

#### Filter by Relation Property Not Matching Value

### query.graph

```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"], ["9"], ["10"], ["11"], ["12"], ["13"], ["14"], ["15"], ["16"], ["17"], ["18"]]}
const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    author: {
      $or: [
        {
          name: {
            $eq: null,
          },
        },
        {
          name: {
            $ne: "John",
          },
        },
      ],
    },
  },
})
```

### useQueryGraphStep

```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"], ["9"], ["10"], ["11"], ["12"], ["13"], ["14"], ["15"], ["16"], ["17"], ["18"]]}
const { data: posts } = useQueryGraphStep({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    author: {
      $or: [
        {
          name: {
            $eq: null,
          },
        },
        {
          name: {
            $ne: "John",
          },
        },
      ],
    },
  },
})
```

To filter by a relationship property whose value doesn't match a specific condition, use an `$or` operator that applies the following conditions:

1. The relationship's property is not set. This is necessary to exclude posts that don't have an author.
2. The relationship's property is not equal to the specific value.

So, in the example above, the query retrieves posts that either don't have an author or have an author whose name is not "John".

***

## Apply Pagination

### query.graph

```ts highlights={[["8", "skip", "The number of records to skip before fetching the results."], ["9", "take", "The number of records to fetch."]]}
const { 
  data: posts,
  metadata: { count, take, skip } = {},
} = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  pagination: {
    skip: 0,
    take: 10,
  },
})
```

### useQueryGraphStep

```ts highlights={[["8", "skip", "The number of records to skip before fetching the results."], ["9", "take", "The number of records to fetch."]]}
const { 
  data: posts,
  metadata,
} = useQueryGraphStep({
  entity: "post",
  fields: ["id", "title"],
  pagination: {
    skip: 0,
    take: 10,
  },
})
```

The `graph` method's object parameter accepts a `pagination` property to configure the pagination of retrieved records.

To paginate the returned records, pass the following properties to `pagination`:

- `skip`: (required to apply pagination) The number of records to skip before fetching the results.
- `take`: The number of records to fetch.

When you provide the pagination fields, the `query.graph` method's returned object has a `metadata` property. Its value is an object having the following properties:

- skip: (\`number\`) The number of records skipped.
- take: (\`number\`) The number of records requested to fetch.
- count: (\`number\`) The total number of records.

### Sort Records

### query.graph

```ts highlights={[["5"], ["6"], ["7"]]}
const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  pagination: {
    order: {
      name: "DESC",
    },
  },
})
```

### useQueryGraphStep

```ts highlights={[["5"], ["6"], ["7"]]}
const { data: posts } = useQueryGraphStep({
  entity: "post",
  fields: ["id", "title"],
  pagination: {
    order: {
      name: "DESC",
    },
  },
})
```

Sorting doesn't work on fields of linked data models from other modules.

To sort returned records, pass an `order` property to `pagination`.

The `order` property is an object whose keys are property names, and values are either:

- `ASC` to sort records by that property in ascending order.
- `DESC` to sort records by that property in descending order.

***

## Retrieve Deleted Records

By default, Query doesn't retrieve deleted records. To retrieve all records including deleted records, you can pass the `withDeleted` property to the `query.graph` method.

The `withDeleted` property is available from [Medusa v2.8.5](https://github.com/medusajs/medusa/releases/tag/v2.8.5).

For example:

### query.graph

```ts highlights={[["4", "withDeleted", "Include deleted posts in the results."]]}
const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  withDeleted: true,
})
```

### useQueryGraphStep

```ts highlights={[["4", "withDeleted", "Include deleted posts in the results."]]}
const { data: posts } = useQueryGraphStep({
  entity: "post",
  fields: ["id", "title"],
  withDeleted: true,
})
```

In the example above, you retrieve all posts, including deleted ones.

### Retrieve Only Deleted Records

To retrieve only deleted records, you can add a `deleted_at` filter and set its value to not `null`. For example:

### query.graph

```ts highlights={withDeletedHighlights}
const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    deleted_at: {
      $ne: null,
    },
  },
  withDeleted: true,
})
```

### useQueryGraphStep

```ts highlights={withDeletedHighlights}
const { data: posts } = useQueryGraphStep({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    deleted_at: {
      $ne: null,
    },
  },
  withDeleted: true,
})
```

In the example above, you retrieve only deleted posts by enabling the `withDeleted` property and adding a filter to only retrieve records where the `deleted_at` property is not `null`.

***

## Retrieve Localized Data

### Prerequisites

- [Medusa v2.12.4 or later](https://github.com/medusajs/medusa/releases/tag/v2.12.4)
- [Translation Module Configured](https://docs.medusajs.com/resources/commerce-modules/translation#configure-translation-module/index.html.md)

To retrieve localized data for data models that have translations, pass a `locale` property in the second parameter object of the `query.graph` method.

### query.graph

```ts highlights={[["7", "locale", "Pass the locale to retrieve localized data."]]}
const { data: products } = await query.graph(
  {
    entity: "product",
    fields: ["id", "title", "description"],
  },
  {
    locale: "fr-FR",
  }
)
```

### useQueryGraphStep

```ts highlights={[["5", "locale", "Pass the locale to retrieve localized data."]]}
const { data: products } = useQueryGraphStep({
  entity: "product",
  fields: ["id", "title", "description"],
  options: {
    locale: "fr-FR",
  },
})
```

The `locale` property is a string representing the locale code following the [IETF BCP 47 standard](https://gist.github.com/typpo/b2b828a35e683b9bf8db91b5404f1bd1).

The returned products will have their `title` and `description` properties in French (`fr-FR`), if translations are available.

Learn more in the [Translation Module](https://docs.medusajs.com/resources/commerce-modules/translation/index.html.md) documentation.

***

## Configure Query to Throw Error

By default, if Query doesn't find records matching your query, it returns an empty array. You can configure Query to throw an error when no records are found.

The `query.graph` method accepts as a second parameter an object that can have a `throwIfKeyNotFound` property. Its value is a boolean indicating whether to throw an error if no record is found when filtering by IDs. By default, it's `false`.

For example:

### query.graph

```ts
const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    id: "post_123",
  },
}, {
  throwIfKeyNotFound: true,
})
```

### useQueryGraphStep

```ts
const { data: posts } = useQueryGraphStep({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    id: "post_123",
  },
  options: {
    throwIfKeyNotFound: true,
  },
})
```

In the example above, if no post is found with the ID `post_123`, Query throws an error. This is useful to stop execution when a record is expected to exist.

### Throw Error on Related Data Model

The `throwIfKeyNotFound` option can also be used to throw an error if the ID of a related data model's record (in the same module) is passed in the filters, and the related record doesn't exist.

For example:

### query.graph

```ts
const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title", "author.*"],
  filters: {
    id: "post_123",
    author_id: "author_123",
  },
}, {
  throwIfKeyNotFound: true,
})
```

### useQueryGraphStep

```ts
const { data: posts } = useQueryGraphStep({
  entity: "post",
  fields: ["id", "title", "author.*"],
  filters: {
    id: "post_123",
    author_id: "author_123",
  },
  options: {
    throwIfKeyNotFound: true,
  },
})
```

In the example above, Query throws an error either if no post is found with the ID `post_123` or if it's found but its author ID isn't `author_123`.

In the above example, it's assumed that a post belongs to an author, so it has an `author_id` property. However, this also works in the opposite case, where an author has many posts.

For example:

### query.graph

```ts
const { data: posts } = await query.graph({
  entity: "author",
  fields: ["id", "name", "posts.*"],
  filters: {
    id: "author_123",
    posts: {
      id: "post_123",
    },
  },
}, {
  throwIfKeyNotFound: true,
})
```

### useQueryGraphStep

```ts
const { data: posts } = useQueryGraphStep({
  entity: "author",
  fields: ["id", "name", "posts.*"],
  filters: {
    id: "author_123",
    posts: {
      id: "post_123",
    },
  },
  options: {
    throwIfKeyNotFound: true,
  },
})
```

In the example above, Query throws an error if no author is found with the ID `author_123` or if the author is found but doesn't have a post with the ID `post_123`.

***

## Cache Query Results

### Prerequisites

- [Caching Module installed with a provider.](https://docs.medusajs.com/resources/infrastructure-modules/caching#install-the-caching-module/index.html.md)

Caching options are available from [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0).

You can cache Query results to improve performance and reduce database load. To do that, you can pass a `cache` property in the second parameter of the `query.graph` method.

For example, to enable caching for a query:

### query.graph

```ts highlights={[["6", "enable", "Enable caching for this query."]]}
const { data: products } = await query.graph({
  entity: "product",
  fields: ["id", "title"],
}, {
  cache: {
    enable: true,
  },
})
```

### useQueryGraphStep

```ts highlights={[["6", "enable", "Enable caching for this query."]]}
const { data: products } = useQueryGraphStep({
  entity: "product",
  fields: ["id", "title"],
  options: {
    cache: {
      enable: true,
    },
  },
})
```

In this example, you enable caching of the query's results. The next time the same query is executed, the results are returned from the cache instead of querying the database.

Refer to the [Caching Module documentation](https://docs.medusajs.com/resources/infrastructure-modules/caching/concepts#caching-best-practices/index.html.md) for best practices on caching.

### Cache Properties

`cache` is an object that accepts the following properties:

- enable: (\`boolean\` | \`((args: any\[]) => boolean | undefined)\`) Whether to enable caching of query results. If a function is passed, it receives as a parameter the \`query.graph\` parameters, and returns a boolean indicating whether caching is enabled.
- key: (\`string\` | \`((args: any\[], cachingModule: ICachingModuleService) => string | Promise\<string>)\`) The key to cache the query results with. If no key is provided, the Caching Module will generate the key from the \`query.graph\` parameters.

  If a function is passed, it receives the following properties:

  1\. The parameters passed to \`query.graph\`.

  2\. The \[Caching Module's service]\(!resources!/references/caching-service), which you can use to perform caching operations.

  The function must return a string indicating the cache key.
- tags: (\`string\[]\` | \`((args: any\[]) => string\[] | undefined)\`) The tags to associate with the cached results. Tags are useful to group related items. If no tag is provided, the Caching Module will generate relevant tags for the entity and its retrieved relations.

  If a function is passed, it receives as a parameter the \`query.index\` parameters, and returns an array of strings indicating the cache tags.
- ttl: (\`number\` | \`((args: any\[]) => number | undefined)\`) The time-to-live (TTL) for the cached results, in seconds. If no TTL is provided, the Caching Module Provider will receive the \[configured TTL of the Caching Module]\(!resources!/infrastructure-modules/caching#caching-module-options), or it will use its own default value.

  If a function is passed, it receives as a parameter the \`query.graph\` parameters, and returns a number indicating the TTL.
- autoInvalidate: (\`boolean\` | \`((args: any\[]) => boolean | undefined)\`) Whether to automatically invalidate the cached data when it expires.

  If a function is passed, it receives as a parameter the \`query.graph\` parameters, and returns a boolean indicating whether to automatically invalidate the cache.
- providers: (\`string\[]\` | \`((args: any\[]) => string\[] | undefined)\`) The IDs of the providers to use for caching. If not provided, the \[default Caching Module Provider]\(!resources!/infrastructure-modules/caching/providers#default-caching-module-provider) is used. If multiple providers are passed, the cache is stored and retrieved in those providers in order.

  If a function is passed, it receives as a parameter the \`query.graph\` parameters, and return an array of strings indicating the providers to use.

### Set Cache Key

By default, the Caching Module generates a cache key for a query based on the arguments passed to `query.graph`. The cache key is a unique key that the cached result is stored with.

Alternatively, you can set a custom cache key for a query. This is useful if you want to manage invalidating the cache manually.

To set the cache key of a query, pass the `cache.key` option:

### query.graph

```ts highlights={[["7"]]}
const { data: products } = await query.graph({
  entity: "product",
  fields: ["id", "title"],
}, {
  cache: {
    enable: true,
    key: "products-123456",
    // to disable auto invalidation:
    // autoInvalidate: false,
  },
})
```

### useQueryGraphStep

```ts highlights={[["7"]]}
const { data: products } = useQueryGraphStep({
  entity: "product",
  fields: ["id", "title"],
  options: {
    cache: {
      enable: true,
      key: "products-123456",
      // to disable auto invalidation:
      // autoInvalidate: false,
    },
  },
})
```

In the example above, you cache the query results with the `products-123456` key.

You should generate cache keys with the Caching Module service's [computeKey method](https://docs.medusajs.com/resources/references/caching-service#computeKey/index.html.md) to ensure that the key is unique and follows best practices.

You can also pass a function as the value of `cache.key`:

Passing a function to `cache.key` is only supported in `query.graph`, not in `useQueryGraphStep`. This is due to variable-related restrictions in workflows, as explained in the [Data Manipulation in Workflows guide](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). You can alternatively create a step that uses Query directly, and use it in the workflow.

```ts highlights={[["7"], ["8"], ["9"], ["10"], ["11"]]}
const { data: products } = await query.graph({
  entity: "product",
  fields: ["id", "title"],
}, {
  cache: {
    enable: true,
    key: async (args, cachingModuleService) => {
      return await cachingModuleService.computeKey({
        ...args,
        prefix: "products",
      })
    },
  },
})
```

In the example above, you pass a function to `key`. It accepts two parameters:

1. The arguments of `query.graph` passed as an array.
2. The [Caching Module's service](https://docs.medusajs.com/resources/references/caching-service/index.html.md).

You generate the key using the [computeKey method of the Caching Module's service](https://docs.medusajs.com/resources/references/caching-service#computeKey/index.html.md). The query results will be cached with that key.

### Set Cache Tags

By default, the Caching Module generates relevant tags for a query based on the entity and its retrieved relations. Cache tags are useful to group related items together, allowing you to [retrieve](https://docs.medusajs.com/resources/references/caching-service#get/index.html.md) or [invalidate](https://docs.medusajs.com/resources/references/caching-service#clear/index.html.md) items by common tags.

Alternatively, you can set the cache tags of a query manually. This is useful if you want to manage invalidating the cache manually, or you want to group related cached items with custom tags.

To set the cache tags of a query, pass the `cache.tags` option:

### query.graph

```ts highlights={[["7"]]}
const { data: products } = await query.graph({
  entity: "product",
  fields: ["id", "title"],
}, {
  cache: {
    enable: true,
    tags: ["Product:list:*"],
  },
})
```

### useQueryGraphStep

```ts highlights={[["7"]]}
const { data: products } = useQueryGraphStep({
  entity: "product",
  fields: ["id", "title"],
  options: {
    cache: {
      enable: true,
      tags: ["Product:list:*"],
      // to disable auto invalidation:
      // autoInvalidate: false,
    },
  },
})
```

In the example above, you cache the query results with the `Product:list:*` tag.

The cache tag must follow the [Caching Tags Convention](https://docs.medusajs.com/resources/infrastructure-modules/caching/concepts#caching-tags-convention/index.html.md) to be automatically invalidated.

You can also pass a function as the value of `cache.tags`:

Passing a function to `cache.tags` is only supported in `query.graph`, not in `useQueryGraphStep`. This is due to variable-related restrictions in workflows, as explained in the [Data Manipulation in Workflows guide](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). You can alternatively create a step that uses Query directly, and use it in the workflow.

```ts highlights={[["7"], ["8"], ["9"], ["10"], ["11"], ["12"], ["13"]]}
const { data: products } = await query.graph({
  entity: "product",
  fields: ["id", "title"],
}, {
  cache: {
    enable: true,
    tags: (args) => {
      const collectionId = args[0].filter?.collection_id
      return [
        ...args,
        collectionId ? `ProductCollection:${collectionId}` : undefined,
      ]
    },
  },
})
```

In the example above, you use a function to determine the cache tags. The function accepts the arguments passed to `query.graph` as an array.

Then, you add the `ProductCollection:id` tag if `collection_id` is passed in the query filters.

### Set TTL

By default, the Caching Module will pass the [configured time-to-live (TTL)](https://docs.medusajs.com/resources/infrastructure-modules/caching#caching-module-options/index.html.md) to the Caching Module Provider when caching data. The Caching Module Provider may also have its own default TTL.  The cache isn't invalidated until the configured TTL passes.

Alternatively, you can set a custom TTL for a query. This is useful if you want the cached data to be invalidated sooner or later than the default TTL.

To set the TTL of the cached query results to a custom value, use the `cache.ttl` option:

### query.graph

```ts highlights={[["7"]]}
const { data: products } = await query.graph({
  entity: "product",
  fields: ["id", "title"],
}, {
  cache: {
    enable: true,
    ttl: 100, // 100 seconds
  },
})
```

### useQueryGraphStep

```ts highlights={[["7"]]}
const { data: products } = useQueryGraphStep({
  entity: "product",
  fields: ["id", "title"],
  options: {
    cache: {
      enable: true,
      ttl: 100, // 100 seconds
    },
  },
})
```

In the example above, you set the TTL of the cached query result to `100` seconds. It will be invalidated after that time.

You can also pass a function as the value of `cache.ttl`:

Passing a function to `cache.ttl` is only supported in `query.graph`, not in `useQueryGraphStep`. This is due to variable-related restrictions in workflows, as explained in the [Data Manipulation in Workflows guide](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). You can alternatively create a step that uses Query directly, and use it in the workflow.

```ts highlights={[["10"], ["11"], ["12"]]}
const { data: products } = await query.graph({
  entity: "product",
  fields: ["id", "title"],
  filters: {
    id: "prod_123",
  },
}, {
  cache: {
    enable: true,
    ttl: (args) => {
      return args[0].filters.id === "test" ? 10 : 100
    },
  },
})
```

In the example above, you use a function to determine the TTL. The function accepts the arguments passed to `query.graph` as an array.

Then, you set the TTL based on the ID of the product passed in the filters.

### Set Auto Invalidation

By default, the Caching Module automatically invalidates cached query results when the data changes.

Alternatively, you can disable auto invalidation of cached query results. This is useful if you want to manage invalidating the cache manually.

To configure invalidation behavior, use the `cache.autoInvalidate` option:

### query.graph

```ts highlights={[["7"]]}
const { data: products } = await query.graph({
  entity: "product",
  fields: ["id", "title"],
}, {
  cache: {
    enable: true,
    autoInvalidate: false,
  },
})
```

### useQueryGraphStep

```ts highlights={[["7"]]}
const { data: products } = useQueryGraphStep({
  entity: "product",
  fields: ["id", "title"],
  options: {
    cache: {
      enable: true,
      autoInvalidate: false,
    },
  },
})
```

In this example, you disable auto invalidation of the query result. You must [invalidate](https://docs.medusajs.com/resources/references/caching-service#clear/index.html.md) the cached data manually.

You can also pass a function as the value of `cache.autoInvalidate`:

Passing a function to `cache.autoInvalidate` is only supported in `query.graph`, not in `useQueryGraphStep`. This is due to variable-related restrictions in workflows, as explained in the [Data Manipulation in Workflows guide](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). You can alternatively create a step that uses Query directly, and use it in the workflow.

```ts highlights={[["7"], ["8"], ["9"]]}
const { data: products } = await query.graph({
  entity: "product",
  fields: ["id", "title"],
}, {
  cache: {
    enable: true,
    autoInvalidate: (args) => {
      return !args[0].fields.includes("custom_field")
    },
  },
})
```

In the example above, you use a function to determine whether to invalidate the cached query result automatically. The function accepts the arguments passed to `query.graph` as an array.

Then, you enable auto-invalidation only if the `fields` passed to `query.graph` don't include `custom_fields`. If this disables auto-invalidation, you must [invalidate](https://docs.medusajs.com/resources/references/caching-service#clear/index.html.md) the cached data manually.

Learn more about automatic invalidation in the [Caching Module documentation](https://docs.medusajs.com/resources/infrastructure-modules/caching/concepts#automatic-cache-invalidation/index.html.md).

### Set Caching Provider

By default, the Caching Module uses the [default Caching Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/caching/providers#default-caching-module-provider/index.html.md) to cache a query.

Alternatively, you can set the caching provider to use for a query. This is useful if you have multiple caching providers configured, and you want to use a specific one for a query, or you want to specify a fallback provider.

To configure the caching providers, use the `cache.providers` option:

### query.graph

```ts highlights={[["7"]]}
const { data: products } = await query.graph({
  entity: "product",
  fields: ["id", "title"],
}, {
  cache: {
    enable: true,
    providers: ["caching-redis", "caching-memcached"],
  },
})
```

### useQueryGraphStep

```ts highlights={[["7"]]}
const { data: products } = useQueryGraphStep({
  entity: "product",
  fields: ["id", "title"],
  options: {
    cache: {
      enable: true,
      providers: ["caching-redis", "caching-memcached"],
    },
  },
})
```

In the example above, you specify the providers with ID `caching-redis` and `caching-memcached` to cache the query results. These IDs must match the IDs of the providers in `medusa-config.ts`.

When you pass multiple providers, the cache is stored and retrieved in those providers in order.

You can also pass a function as the value of `cache.providers`:

Passing a function to `cache.providers` is only supported in `query.graph`, not in `useQueryGraphStep`. This is due to variable-related restrictions in workflows, as explained in the [Data Manipulation in Workflows guide](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). You can alternatively create a step that uses Query directly, and use it in the workflow.

```ts highlights={[["10"], ["11"], ["12"]]}
const { data: products } = await query.graph({
  entity: "product",
  fields: ["id", "title"],
  filters: {
    id: "prod_123",
  },
}, {
  cache: {
    enable: true,
    providers: (args) => {
      return args[0].filters.id === "test" ? ["caching-redis"] : ["caching-memcached"]
    },
  },
})
```

In the example above, you use a function to determine the caching providers. The function accepts the arguments passed to `query.graph` as an array.

Then, you set the providers based on the ID of the product passed in the filters.

***

## Request Query Configurations

For API routes that retrieve a single or list of resources, Medusa provides a `validateAndTransformQuery` middleware that:

- Validates accepted query parameters, as explained in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md).
- Parses configurations that are received as query parameters to be passed to Query.

Using this middleware allows you to have default configurations for retrieved fields and relations or pagination, while allowing clients to customize them per request.

### Step 1: Add Middleware

The first step is to use the `validateAndTransformQuery` middleware on the `GET` route. You add the middleware in `src/api/middlewares.ts`:

```ts title="src/api/middlewares.ts"
import { 
  validateAndTransformQuery,
  defineMiddlewares,
} from "@medusajs/framework/http"
import { createFindParams } from "@medusajs/medusa/api/utils/validators"

export const GetCustomSchema = createFindParams()

export default defineMiddlewares({
  routes: [
    {
      matcher: "/customs",
      method: "GET",
      middlewares: [
        validateAndTransformQuery(
          GetCustomSchema,
          {
            defaults: [
              "id",
              "title",
              "products.*",
            ],
            isList: true,
          }
        ),
      ],
    },
  ],
})
```

The `validateAndTransformQuery` accepts two parameters:

1. A Zod validation schema for the query parameters, which you can learn more about in the [API Route Validation documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md). Medusa has a `createFindParams` utility that generates a Zod schema that accepts four query parameters:
   1. `fields`: The fields and relations to retrieve in the returned resources.
   2. `offset`: The number of items to skip before retrieving the returned items.
   3. `limit`: The maximum number of items to return.
   4. `order`: The fields to order the returned items by in ascending or descending order.
2. A Query configuration object. It accepts the following properties:
   1. `defaults`: An array of default fields and relations to retrieve in each resource.
   2. `isList`: A boolean indicating whether a list of items is returned in the response.
   3. `allowed`: An array of fields and relations allowed to be passed in the `fields` query parameter.
   4. `defaultLimit`: A number indicating the default limit to use if no limit is provided. By default, it's `50`.

### Step 2: Use Configurations in API Route

After applying this middleware, your API route now accepts the `fields`, `offset`, `limit`, and `order` query parameters mentioned above.

The middleware transforms these parameters to configurations that you can pass to Query in your API route handler. These configurations are stored in the `queryConfig` parameter of the `MedusaRequest` object.

As of [Medusa v2.2.0](https://github.com/medusajs/medusa/releases/tag/v2.2.0), `remoteQueryConfig` has been deprecated in favor of `queryConfig`. Their usage is still the same, only the property name has changed.

For example, create the file `src/api/customs/route.ts` with the following content:

```ts title="src/api/customs/route.ts"
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import {
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: posts } = await query.graph({
    entity: "post",
    ...req.queryConfig,
  })

  res.json({ posts: posts })
}
```

This adds a `GET` API route at `/customs`, which is the API route you added the middleware for.

In the API route, you pass `req.queryConfig` to `query.graph`. `queryConfig` has properties like `fields` and `pagination` to configure the query based on the default values you specified in the middleware, and the query parameters passed in the request.

### Test it Out

To test it out, start your Medusa application and send a `GET` request to the `/customs` API route. A list of records is retrieved with the specified fields in the middleware.

```json title="Returned Data"
{
  "posts": [
    {
      "id": "123",
      "title": "test"
    }
  ]
}
```

Try passing one of the Query configuration parameters, like `fields` or `limit`, and you'll see its impact on the returned result.

Learn more about [specifying fields and relations](https://docs.medusajs.com/api/store#select-fields-and-relations) and [pagination](https://docs.medusajs.com/api/store#pagination) in the API reference.


# Read-Only Module Link

In this chapter, you’ll learn what a read-only module link is and how to define one.

## What is a Read-Only Module Link?

Consider a scenario where you need to access related records from another module, but don't want the overhead of managing or storing the links between them. This can include cases where you're working with external data models not stored in your Medusa database, such as third-party systems.

In those cases, instead of defining a [Module Link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) whose linked records must be stored in a link table in the database, you can use a read-only module link. A read-only module link builds a virtual relation from one data model to another in a different module without creating a link table in the database. Instead, the linked record's ID is stored in the first data model's field.

For example, Medusa creates a read-only module link from the `Cart` data model of the [Cart Module](https://docs.medusajs.com/resources/commerce-modules/cart/index.html.md) to the `Customer` data model of the [Customer Module](https://docs.medusajs.com/resources/commerce-modules/customer/index.html.md). This link allows you to access the details of the cart's customer without managing the link. Instead, the customer's ID is stored in the `Cart` data model.

![Diagram illustrating the read-only module link from cart to customer](https://res.cloudinary.com/dza7lstvk/image/upload/v1742212508/Medusa%20Book/cart-customer_w6vk59.jpg)

***

## How to Define a Read-Only Module Link

The `defineLink` function accepts an optional third-parameter object that can hold additional configurations for the module link.

If you're not familiar with the `defineLink` function, refer to the [Module Links chapter](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) for more information.

To make the module link read-only, pass the `readOnly` property as `true`. You must also set in the link configuration of the first data model a `field` property that specifies the data model's field where the linked record's ID is stored.

For example:

```ts highlights={highlights}
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: BlogModule.linkable.post,
    field: "product_id",
  },
  ProductModule.linkable.product,
  {
    readOnly: true,
  }
)
```

In this example, you define a read-only module link from the Blog Module's `post` data model to the Product Module's `product` data model. You do that by:

- Passing an object as a first parameter that accepts the linkable configuration and the field where the linked record's ID is stored.
- Setting the `readOnly` property to `true` in the third parameter.

Unlike the stored module link, Medusa will not create a table in the database for this link. Instead, Medusa uses the ID stored in the specified field of the first data model to retrieve the linked record.

***

## Retrieve Read-Only Linked Record

[Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) allows you to retrieve records linked through a read-only module link.

For example, assuming you have the module link created in [the above section](#how-to-define-a-read-only-module-link), you can retrieve a post and its linked product as follows:

```ts
const { result } = await query.graph({
  entity: "post",
  fields: ["id", "product.*"],
  filters: {
    id: "post_123",
  },
})
```

In the above example, you retrieve a post and its linked product. Medusa will use the ID of the product in the post's `product_id` field to determine which product should be retrieved.

***

## Read-Only Module Link Direction

A read-only module is uni-directional. So, you can only retrieve the linked record from the first data model. If you need to access the linked record from the second data model, you must define another read-only module link in the opposite direction.

In the `blog` -> `product` example, you can access a post's product, but you can't access a product's posts. You would have to define another read-only module link from `product` to `blog` to access a product's posts.

***

## Inverse Read-Only Module Link

An inverse read-only module link is a read-only module link that allows you to access the linked record based on the ID stored in the second data model.

For example, consider you want to access a product's posts. You can define a read-only module link from the Product Module's `product` data model to the Blog Module's `post` data model:

```ts
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    field: "id",
  },
  {
    ...BlogModule.linkable.post.id,
    primaryKey: "product_id",
  },
  {
    readOnly: true,
  }
)
```

In the above example, you define a read-only module link from the Product Module's `product` data model to the Blog Module's `post` data model. This link allows you to access a product's posts.

Since you can't add a `post_id` field to the `product` data model, you must:

1. Set the `field` property in the first data model's link configuration to the product's ID field.
2. Spread the `BlogModule.linkable.post.id` object in the second parameter object and set the `primaryKey` property to the field in the `post` data model that holds the product's ID.

You can now retrieve a product and its linked posts:

```ts
const { result } = await query.graph({
  entity: "product",
  fields: ["id", "post.*"],
  filters: {
    id: "prod_123",
  },
})
```

***

## One-to-One or One-to-Many?

When you retrieve the linked record through a read-only module link, the retrieved data may be an object (one-to-one) or an array of objects (one-to-many) based on different criteria.

|Scenario|Relation Type|
|---|---|---|
|The first data model's |One-to-one relation|
|The first data model's |One-to-many relation|
|The read-only module link is inversed.|One-to-many relation if multiple records in the second data model have the same ID of the first data model. Otherwise, one-to-one relation.|

### One-to-One Relation

Consider the first read-only module link you defined in this chapter:

```ts
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"

export default defineLink(
  {
    linkable: BlogModule.linkable.post,
    field: "product_id",
  },
  ProductModule.linkable.product,
  {
    readOnly: true,
  }
)
```

Since the `product_id` field of a post stores the ID of a single product, the link is a one-to-one relation. When querying a post, you'll get a single product object:

```json title="Example Data"
[
  {
    "id": "post_123",
    "product_id": "prod_123",
    "product": {
      "id": "prod_123",
      // ...
    }
  }
]
```

### One-to-Many Relation

Consider the read-only module link from the `post` data model uses an array of product IDs:

```ts
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"

export default defineLink(
  {
    linkable: BlogModule.linkable.post,
    field: "product_ids",
  },
  ProductModule.linkable.product,
  {
    readOnly: true,
  }
)
```

Where `product_ids` in the `post` data model is an array of strings. In this case, the link would be a one-to-many relation. So, an array of products would be returned when querying a post:

```json title="Example Data"
[
  {
    "id": "post_123",
    "product_ids": ["prod_123", "prod_124"],
    "product": [
      {
        "id": "prod_123",
        // ...
      },
      {
        "id": "prod_124",
        // ...
      }
    ]
  }
]
```

### Relation with Inversed Read-Only Link

If you define an inversed read-only module link where the ID of the linked record is stored in the second data model, the link can be either one-to-one or one-to-many based on the number of records in the second data model that have the same ID of the first data model.

For example, consider the `product` -> `post` link you defined in an earlier section:

```ts
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    field: "id",
  },
  {
    ...BlogModule.linkable.post.id,
    primaryKey: "product_id",
  },
  {
    readOnly: true,
  }
)
```

In the above snippet, the ID of the product is stored in the `post`'s `product_id` string field.

When you retrieve the post of a product, it may be a post object, or an array of post objects if multiple posts are linked to the product:

```json title="Example Data"
[
  {
    "id": "prod_123",
    "post": {
      "id": "post_123",
      "product_id": "prod_123"
      // ...
    }
  },
  {
    "id": "prod_321",
    "post": [
      {
        "id": "post_123",
        "product_id": "prod_321"
        // ...
      },
      {
        "id": "post_124",
        "product_id": "prod_321"
        // ...
      }
    ]
  }
]
```

If, however, you use an array field in `post`, the relation would always be one-to-many:

```json title="Example Data"
[
  {
    "id": "prod_123",
    "post": [
      {
        "id": "post_123",
        "product_id": "prod_123"
        // ...
      }
    ]
  }
]
```

#### Force One-to-Many Relation

Alternatively, you can force a one-to-many relation by setting `isList` to `true` in the first data model's link configuration. For example:

```ts
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    field: "id",
    isList: true,
  },
  {
    ...BlogModule.linkable.post.id,
    primaryKey: "product_id",
  },
  {
    readOnly: true,
  }
)
```

In this case, the relation would always be one-to-many, even if only one post is linked to a product:

```json title="Example Data"
[
  {
    "id": "prod_123",
    "post": [
      {
        "id": "post_123",
        "product_id": "prod_123"
        // ...
      }
    ]
  }
]
```

### Relation Name for Inverse Read-Only Links

Whether you're using a one-to-one or one-to-many relation for an inverse read-only module link, the relation name is always the alias of the second data model. It's not changed to its plural form for one-to-many relations.

For example, looking again at the forced one-to-many relation example:

```ts
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    field: "id",
    isList: true,
  },
  {
    ...BlogModule.linkable.post.id,
    primaryKey: "product_id",
  },
  {
    readOnly: true,
  }
)
```

When querying a product, the relation name is still `post`, not `posts`:

```ts highlights={relationNameHighlights}
const { result } = await query.graph({
  entity: "product",
  fields: ["id", "post.*"],
  filters: {
    id: "prod_123",
  },
})
```

Consequently, the result will include a `post` property, even though it's an array of posts:

```json title="Example Data"
[
  {
    "id": "prod_123",
    "post": [
      {
        "id": "post_123",
        "product_id": "prod_123"
        // ...
      }
    ]
  }
]
```

***

## Example: Read-Only Module Link for Virtual Data Models

Read-only module links are most useful when working with data models that aren't stored in your Medusa database. For example, data that is stored in a third-party system.

In those cases, you can define a read-only module link between a data model in Medusa and the data model in the external system, facilitating the retrieval of the linked data.

To define the read-only module link to a virtual data model, you must:

1. Define the read-only module link from the Medusa data model to the virtual data model.
2. Create a `list` method in the custom module's service. This method retrieves the linked records filtered by the ID(s) of the Medusa data model.
   - You can also create a `listAndCount` method to retrieve the related records with pagination.
3. Use Query to retrieve the Medusa data model and its linked records from the virtual data model.

For example, consider you have a CMS Module that integrates a third-party Content-Management System (CMS) with Medusa, and you want to retrieve the posts in the CMS associated with a product in Medusa. The next steps showcase how to implement this.

### a. Define Read-Only Module Link

Start by defining a read-only module link from the `Product` data model in Medusa to the external `post` data model in the CMS:

```ts title="src/links/product-cms.ts" highlights={linkHighlights}
import { defineLink } from "@medusajs/framework/utils"
import ProductModule from "@medusajs/medusa/product"
import { CMS_MODULE } from "../modules/cms"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    field: "id",
  },
  {
    linkable: {
      serviceName: CMS_MODULE,
      alias: "cms_post",
      primaryKey: "product_id",
    },
  },
  {
    readOnly: true,
  }
)
```

To define the read-only module link, you must pass to `defineLink`:

1. An object with the linkable configuration of the data model in Medusa, and the fields that will be passed as a filter to the CMS service.
   - For example, if you want to filter by product title instead, you can pass `title` instead of `id`.
2. An object with the linkable configuration of the virtual data model in the CMS. This object must have the following properties:
   - `serviceName`: The name of the service, which is the CMS Module's name. Medusa uses this name to resolve the module's service from the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md).
   - `alias`: The alias to use when querying the linked records. You'll see how that works in a bit.
   - `primaryKey`: The field in the CMS data model that holds the ID of a product.
3. The third parameter: an object with the `readOnly` property set to `true`.

### b. Add List Methods to CMS Module Service

Next, add the following methods to the CMS Module's service:

```ts title="src/modules/cms/service.ts" highlights={serviceHighlights}
import { FindConfig } from "@medusajs/framework/types"

export default class CmsModuleService {
  private client
  // ...

  async list(
    filter: {
      product_id: string | string[]
    }
  ) {
    return this.client.getPosts(filter)
    /**
     - Example of returned data:
     - 
     - [
     -   {
     -     "id": "post_123",
     -     "product_id": "prod_321"
     -   },
     -   {
     -     "id": "post_456",
     -     "product_id": "prod_654"
     -   }
     - ]
    */
  }

  // To retrieve with pagination
  async listAndCount(
    filter: {
      product_id: string | string[]
    },
    config?: FindConfig<any> | undefined 
  ) {
    return this.client.getPosts(filter, {
      limit: config?.take,
      offset: config?.skip,
    })
    /**
     - Example of returned data:
     - 
     - {
     -   count: 2,
     -   data: [
     -     {
     -       "id": "post_123",
     -       "product_id": "prod_321"
     -     },
     -     {
     -       "id": "post_456",
     -       "product_id": "prod_654"
     -     }
     -   ]
     - }
    */
  }
}
```

To retrieve the linked records, you must implement a `list` method in the CMS Module's service.

The `list` method accepts an object of filters holding the ID(s) of the products to retrieve their associated posts. The name of the filter property must match the `primary_key` defined in the link configuration, which is `product_id` in this case.

The returned posts must include the product's ID in a field with the same name as the `primary_key` option in the link configurations, which is `product_id` in this case.

You can also create a `listAndCount` method to retrieve the posts with pagination. This method is called if you pass [pagination parameters to Query](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-pagination/index.html.md).

### c. Query Linked Records

Now, you can use Query to retrieve a product and its linked post from the CMS:

```ts highlights={queryHighlights}
const { data } = await query.graph({
  entity: "product",
  fields: ["id", "cms_post.*"],
})
```

In the above example, you pass `cms_post.*` in the fields, which is the `alias` of the virtual data model in the [link configuration](#a-define-read-only-module-link).

Each product will have a `cms_post` field that holds the posts whose `product_id` matches the product's ID. For example:

```json title="Example Data"
[
  {
    "id": "prod_123",
    "cms_post": {
      "id": "post_123",
      "product_id": "prod_123",
      // ...
    }
  }
]
```

If multiple posts have their `product_id` set to a product's ID, an array of posts is returned instead:

```json title="Example Data"
[
  {
    "id": "prod_123",
    "cms_post": [
      {
        "id": "post_123",
        "product_id": "prod_123",
        // ...
      },
      {
        "id": "post_124",
        "product_id": "prod_123",
        // ...
      }
    ]
  }
]
```

[Sanity Integration Tutorial](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md).


# Commerce Modules

In this chapter, you'll learn about Medusa's Commerce Modules.

## What is a Commerce Module?

Commerce Modules are built-in [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) of Medusa that provide core commerce logic specific to domains like Products, Orders, Customers, Fulfillment, and much more.

Medusa's Commerce Modules are used to form Medusa's default [workflows](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) and [APIs](https://docs.medusajs.com/api/store). For example, when you call the add to cart endpoint. the add to cart workflow runs which uses the Product Module to check if the product exists, the Inventory Module to ensure the product is available in the inventory, and the Cart Module to finally add the product to the cart.

You'll find the details and steps of the add-to-cart workflow in [this workflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/addToCartWorkflow/index.html.md)

The core commerce logic contained in Commerce Modules is also available directly when you are building customizations. This granular access to commerce functionality is unique and expands what's possible to build with Medusa drastically.

### List of Medusa's Commerce Modules

Refer to [this reference](https://docs.medusajs.com/resources/commerce-modules/index.html.md) for a full list of Commerce Modules in Medusa.

***

## Use Commerce Modules in Custom Flows

Similar to your [custom modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), the Medusa application registers a Commerce Module's service in the [container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md). So, you can resolve it in your custom flows. This is useful as you build unique requirements extending core commerce features.

For example, consider you have a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) (a special function that performs a task in a series of steps with rollback mechanism) that needs a step to retrieve the total number of products. You can create a step in the workflow that resolves the Product Module's service from the container to use its methods:

```ts highlights={highlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

export const countProductsStep = createStep(
  "count-products",
  async ({ }, { container }) => {
    const productModuleService = container.resolve("product")

    const [,count] = await productModuleService.listAndCountProducts()

    return new StepResponse(count)
  }
)
```

Your workflow can use services of both custom and Commerce Modules, supporting you in building custom flows without having to re-build core commerce features.


# Module Container

In this chapter, you'll learn about the module container and how to resolve resources from it.

Since modules are [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), each module has a local container used only by the resources of that module.

So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container, and some Framework tools that the Medusa application registers in the module's container.

### List of Registered Resources

Find a list of resources or dependencies registered in a module's container in [the Container Resources reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md).

***

## Resolve Resources from the Module Container

### Resolve in Services

A service's constructor accepts as a first parameter an object used to resolve resources registered in the module's container. To resolve a resource, add the resource's registration name as a property of the object.

For example, to resolve the [Logger](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) from the container:

```ts highlights={[["4"], ["10"]]}
import { Logger } from "@medusajs/framework/types"

type InjectedDependencies = {
  logger: Logger
}

export default class BlogModuleService {
  protected logger_: Logger

  constructor({ logger }: InjectedDependencies) {
    this.logger_ = logger

    this.logger_.info("[BlogModuleService]: Hello World!")
  }

  // ...
}
```

You can then use the logger in the service's methods.

### Resolve in Loaders

[Loaders](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md) accept an object parameter with the property `container`. Its value is the module's container that can be used to resolve resources using its `resolve` method.

For example, to resolve the [Logger](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) in a loader:

```ts highlights={[["9"]]}
import {
  LoaderOptions,
} from "@medusajs/framework/types"
import { 
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export default async function helloWorldLoader({
  container,
}: LoaderOptions) {
  const logger = container.resolve(ContainerRegistrationKeys.LOGGER)

  logger.info("[helloWorldLoader]: Hello, World!")
}
```

You can then use the logger in the loader's code.

***

## Caveat: Resolving Module Services in Loaders

Consider a module that has a main service `BrandModuleService`, and an internal service `CmsService`. Medusa will register both of these services in the module's container.

However, loaders are executed before any services are initialized and registered in the module's container. So, you can't resolve the `BrandModuleService` and `CmsService` in a loader.

Instead, if your main service extends the `MedusaService` [service factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md), you can resolve the internal services generated for each data model passed to the `MedusaService` function.

For example, if the `BrandModuleService` is defined as follows:

```ts
import { MedusaService } from "@medusajs/framework/utils"
import Brand from "./models/brand"

class BrandModuleService extends MedusaService({
  Brand,
}) {
}

export default BrandModuleService
```

Then, you can resolve the `brandService` that allows you to manage brands in the module's loader:

```ts
import {
  LoaderOptions,
} from "@medusajs/framework/types"

export default async function helloWorldLoader({
  container,
}: LoaderOptions) {
  const brandService = container.resolve("brandService")

  const brands = await brandService.list()

  console.log("[helloWorldLoader]: Brands:", brands)
}
```

Refer to the [Service Factory reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for details on the available methods in the generated services.

***

## Alternative to Resolving Other Modules' Services

Since modules are [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), you can't resolve resources that belong to other modules from the module's container. For example, you can't resolve the Product Module's service in the Blog Module's service.

Instead, to build commerce features that span multiple modules, you can create [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). In those workflows, you can resolve services of all modules registered in the Medusa application, including the services of the Product and Blog modules.

Then, you can execute the workflows in [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), or [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md).

Learn more and find examples in the [Module Isolation](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md) chapter.

***

## Avoid Circular Dependencies

When resolving resources in a module's services, make sure you don't create circular dependencies. For example, if `BlogModuleService` resolves `CmsService`, and `CmsService` resolves `BlogModuleService`, it will cause a circular dependency error.

Instead, you should generally only resolve services within the main service. For example, `BlogModuleService` can resolve `CmsService`, but `CmsService` should not resolve `BlogModuleService`.


# Perform Database Operations in a Service

In this chapter, you'll learn how to perform database operations in a module's service.

This chapter is intended for more advanced database use-cases where you need more control over queries and operations. For basic database operations, such as creating or retrieving data of a model, use the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) instead.

## Run Queries

[MikroORM's entity manager](https://mikro-orm.io/docs/entity-manager) is a class that has methods to run queries on the database and perform operations.

Medusa provides an `InjectManager` decorator from the Modules SDK that injects a service's method with a [forked entity manager](https://mikro-orm.io/docs/identity-map#forking-entity-manager).

So, to run database queries in a service:

1. Add the `InjectManager` decorator to the method.
2. Add as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator from the Modules SDK. This context holds database-related context, including the manager injected by `InjectManager`

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 { 
  InjectManager,
  MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@medusajs/framework/mikro-orm/knex"

class BlogModuleService {
  // ...

  @InjectManager()
  async getCount(
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<number | undefined> {
    return await sharedContext?.manager?.count("my_custom")
  }
  
  @InjectManager()
  async getCountSql(
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<number> {
    const data = await sharedContext?.manager?.execute(
      "SELECT COUNT(*) as num FROM my_custom"
    ) 
    
    return parseInt(data?.[0].num || 0)
  }
}
```

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 `@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.

Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods.

***

## Perform Database Operations

There are two ways to perform database operations in transactional methods:

1. Using the [data model repositories](#perform-database-operations-with-data-model-repositories) in your module.
2. Using the [transactional entity manager](#perform-database-operations-with-the-transactional-entity-manager) injected into the method's shared context.

For both approaches, you must wrap the method performing the database operations in a transaction.

### Wrap Database Operations in Transactions

When performing database operations without using the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md), you must wrap the method performing the database operations in a transaction.

To wrap database operations in a transaction, you create two methods:

1. A private or protected method that's wrapped in a transaction. To wrap it in a transaction, you use the `InjectTransactionManager` decorator from the Modules SDK.
2. A public method that calls the transactional method. You use on it the `InjectManager` decorator as explained in the previous section.

Both methods must accept as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator from the Modules SDK. It holds database-related contexts passed through the Medusa application.

For example:

```ts highlights={opHighlights}
import { 
  InjectManager,
  InjectTransactionManager,
  MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@medusajs/framework/mikro-orm/knex"

class BlogModuleService {
  // ...
  @InjectTransactionManager()
  protected async update_(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<any> {
    const transactionManager = sharedContext?.transactionManager
    
    // TODO: update the record
  }

  @InjectManager()
  async update(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ) {
    return await this.update_(input, sharedContext)
  }
}
```

The `BlogModuleService` has two methods:

- A protected `update_` that performs the database operations inside a transaction.
- A public `update` that executes the transactional protected method.

You can then perform in the transactional method the database operations either using the [data model repository](#perform-database-operations-with-data-model-repositories) or the [transactional entity manager](#perform-database-operations-with-the-transactional-entity-manager).

#### Why Wrap a Transactional Method

The variables in the transactional method (such as `update_`) hold values that are uncommitted to the database. They're only committed once the method finishes execution.

So, if in your method you perform database operations, then use their result to perform other actions, such as connecting to a third-party service, you'll be working with uncommitted data.

By placing only the database operations in a method that has the `InjectTransactionManager` and using it in a wrapper method, the wrapper method receives the committed result of the transactional method.

This is also useful if you perform heavy data normalization outside of the database operations. In that case, you don't hold the transaction for a longer time than needed.

For example, the `update` method may call other methods than `update_` to perform other actions:

```ts
// other imports...
import { 
  InjectManager,
  InjectTransactionManager,
  MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@medusajs/framework/mikro-orm/knex"

class BlogModuleService {
  // ...
  @InjectTransactionManager()
  protected async update_(
    // ...
  ): Promise<any> {
    // ...
  }
  @InjectManager()
  async update(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ) {
    const newData = await this.update_(input, sharedContext)

    // example method that sends data to another system
    await this.sendNewDataToSystem(newData)

    return newData
  }
}
```

In this case, only the `update_` method is wrapped in a transaction. The returned value `newData` holds the committed result, which can be used for other operations, such as passed to a `sendNewDataToSystem` method.

#### Using Methods in Transactional Methods

If your protected transactional method uses other methods that accept a Medusa context, pass the shared context to those methods.

For example:

```ts highlights={anotherMethodHighlights}
// other imports...
import { 
  InjectTransactionManager,
  MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@medusajs/framework/mikro-orm/knex"

class BlogModuleService {
  // ...
  @InjectTransactionManager()
  protected async anotherMethod(
    @MedusaContext() sharedContext?: Context<EntityManager>
  ) {
    // ...
  }
  
  @InjectTransactionManager()
  protected async update_(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<any> {
    this.anotherMethod(sharedContext)
  }
}
```

You use the `anotherMethod` transactional method in the `update_` transactional method, so you pass it the shared context.

The `anotherMethod` now runs in the same transaction as the `update_` method.

### Perform Database Operations with Data Model Repositories

For every data model in your module, Medusa generates a data model repository that has methods to perform database operations.

For example, if your module has a `Post` model, it has a `postRepository` in the container.

The data model repository is a wrapper around the [entity manager](https://mikro-orm.io/api/knex/class/EntityManager) that provides a higher-level API for performing database operations.

To use the low-level entity manager, use the [transactional entity manager](#perform-database-operations-with-the-transactional-entity-manager) instead.

#### Resolve Data Model Repository

When the Medusa application injects a data model repository into a module's container, it formats the registration name by:

- Taking the data model's name that's passed as the first parameter of `model.define`
- Lower-casing the first character
- Suffixing the result with `Repository`.

For example:

- `Post` model: `postRepository`
- `My_Custom` model: `my_CustomRepository`

So, to resolve a data model repository from a module's container, pass the expected registration name of the repository in the first parameter of the module's constructor (the container).

For example:

### Extending Service Factory

```ts highlights={serviceFactoryRepoHighlights}
import { MedusaService } from "@medusajs/framework/utils"
import { InferTypeOf, DAL } from "@medusajs/framework/types"
import Post from "./models/post"

type Post = InferTypeOf<typeof Post>

type InjectedDependencies = {
  postRepository: DAL.RepositoryService<Post>
}

class BlogModuleService extends MedusaService({
  Post,
}){
  protected postRepository_: DAL.RepositoryService<Post>

  constructor({ 
    postRepository, 
  }: InjectedDependencies) {
    super(...arguments)
    this.postRepository_ = postRepository
  }
}

export default BlogModuleService
```

### Without Service Factory

```ts highlights={noServiceFactoryRepoHighlights}
import { InferTypeOf, DAL } from "@medusajs/framework/types"
import Post from "./models/post"

type Post = InferTypeOf<typeof Post>

type InjectedDependencies = {
  postRepository: DAL.RepositoryService<Post>
}

class BlogModuleService {
  protected postRepository_: DAL.RepositoryService<Post>

  constructor({ 
    postRepository, 
  }: InjectedDependencies) {
    super(...arguments)
    this.postRepository_ = postRepository
  }
}

export default BlogModuleService
```

You can then use the data model repository in your service to perform database operations.

#### Data Model Repository Methods

A data model repository has methods that allows you to create, update, and delete records, among other operations.

To learn about the methods available in a data model repository, refer to the [Data Model Repository](https://docs.medusajs.com/resources/data-model-repository-reference/index.html.md) reference.

### Perform Database Operations with the Transactional Entity Manager

Your transactional method can use the transactional entity manager injected into the method's shared context to perform database operations. It's an instance of the [MikroORM EntityManager](https://mikro-orm.io/api/knex/class/EntityManager) class.

To use an easier higher-level API focused on each data model, use the [data model repository](#perform-database-operations-with-data-model-repositories) instead.

For example:

```ts highlights={transactionalEntityManagerHighlights}
import { 
  InjectManager,
  InjectTransactionManager,
  MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@medusajs/framework/mikro-orm/knex"

class BlogModuleService {
  // ...
  @InjectTransactionManager()
  protected async update_(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<any> {
    const transactionManager = sharedContext?.transactionManager
    await transactionManager?.nativeUpdate(
      "my_custom",
      {
        id: input.id,
      },
      {
        name: input.name,
      }
    )

    // retrieve again
    const updatedRecord = await transactionManager?.execute(
      `SELECT * FROM my_custom WHERE id = '${input.id}'`
    )

    return updatedRecord
  }

  @InjectManager()
  async update(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ) {
    return await this.update_(input, sharedContext)
  }
}
```

The `update_` method uses the transactional entity manager injected into the `sharedContext.transactionManager` property to perform the database operations.

Find all available methods in the [MikroORM EntityManager](https://mikro-orm.io/api/knex/class/EntityManager) reference.

***

## Configure Transactions with the Base Repository

To configure the transaction, such as its [isolation level](https://www.postgresql.org/docs/current/transaction-iso.html), use the `baseRepository` class registered in your module's container.

The `baseRepository` is an instance of a repository class that provides methods to create transactions, run database operations, and more.

The `baseRepository` has a `transaction` method that allows you to run a function within a transaction and configure that transaction.

For example, resolve the `baseRepository` in your service's constructor:

### Extending Service Factory

```ts highlights={baseRepoHighlights}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"
import { DAL } from "@medusajs/framework/types"

type InjectedDependencies = {
  baseRepository: DAL.RepositoryService
}

class BlogModuleService extends MedusaService({
  Post,
}){
  protected baseRepository_: DAL.RepositoryService

  constructor({ baseRepository }: InjectedDependencies) {
    super(...arguments)
    this.baseRepository_ = baseRepository
  }
}

export default BlogModuleService
```

### Without Service Factory

```ts highlights={noServiceFactoryBaseRepoHighlights}
import { DAL } from "@medusajs/framework/types"

type InjectedDependencies = {
  baseRepository: DAL.RepositoryService
}

class BlogModuleService {
  protected baseRepository_: DAL.RepositoryService

  constructor({ baseRepository }: InjectedDependencies) {
    this.baseRepository_ = baseRepository
  }
}

export default BlogModuleService
```

Then, use it in the service's transactional methods:

```ts highlights={repoHighlights}
// ...
import { 
  InjectManager,
  InjectTransactionManager,
  MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@medusajs/framework/mikro-orm/knex"

class BlogModuleService {
  // ...
  @InjectTransactionManager()
  protected async update_(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<any> {
    return await this.baseRepository_.transaction(
      async (transactionManager) => {
        await transactionManager.nativeUpdate(
          "my_custom",
          {
            id: input.id,
          },
          {
            name: input.name,
          }
        )

        // retrieve again
        const updatedRecord = await transactionManager.execute(
          `SELECT * FROM my_custom WHERE id = '${input.id}'`
        )

        return updatedRecord
      },
      {
        transaction: sharedContext?.transactionManager,
      }
    )
  }

  @InjectManager()
  async update(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ) {
    return await this.update_(input, sharedContext)
  }
}
```

The `update_` method uses the `baseRepository_.transaction` method to wrap a function in a transaction.

The function parameter receives a transactional entity manager as a parameter. Use it to perform the database operations.

The `baseRepository_.transaction` method also receives as a second parameter an object of options. You must pass in it the `transaction` property and set its value to the `sharedContext.transactionManager` property so that the function wrapped in the transaction uses the injected transaction manager.

Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods.

### Transaction Options

The second parameter of the `baseRepository_.transaction` method is an object of options that accepts the following properties:

1. `transaction`: Set the transactional entity manager passed to the function. You must provide this option as explained in the previous section.

```ts highlights={[["24"]]}
// other imports...
import { EntityManager } from "@medusajs/framework/mikro-orm/knex"
import { 
  InjectTransactionManager,
  MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"

class BlogModuleService {
  // ...
  @InjectTransactionManager()
  async update_(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<any> {
    return await this.baseRepository_.transaction<EntityManager>(
      async (transactionManager) => {
        // ...
      },
      {
        transaction: sharedContext?.transactionManager,
      }
    )
  }
}
```

2. `isolationLevel`: Sets the transaction's [isolation level](https://www.postgresql.org/docs/current/transaction-iso.html). Its values can be:
   - `read committed`
   - `read uncommitted`
   - `snapshot`
   - `repeatable read`
   - `serializable`

```ts highlights={[["25"]]}
// other imports...
import { 
  InjectTransactionManager,
  MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@medusajs/framework/mikro-orm/knex"
import { IsolationLevel } from "@medusajs/framework/mikro-orm/core"

class BlogModuleService {
  // ...
  @InjectTransactionManager()
  async update_(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<any> {
    return await this.baseRepository_.transaction<EntityManager>(
      async (transactionManager) => {
        // ...
      },
      {
        isolationLevel: IsolationLevel.READ_COMMITTED,
      }
    )
  }
}
```

3. `enableNestedTransactions`: (default: `false`) whether to allow using nested transactions.
   - If `transaction` is provided and this is disabled, the manager in `transaction` is re-used.

```ts highlights={[["24"]]}
// other imports...
import { 
  InjectTransactionManager,
  MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@medusajs/framework/mikro-orm/knex"

class BlogModuleService {
  // ...
  @InjectTransactionManager()
  async update_(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<any> {
    return await this.baseRepository_.transaction<EntityManager>(
      async (transactionManager) => {
        // ...
      },
      {
        enableNestedTransactions: false,
      }
    )
  }
}
```


# Infrastructure Modules

In this chapter, you’ll learn about Infrastructure Modules.

## What is an Infrastructure Module?

An Infrastructure Module implements features and mechanisms related to the Medusa application’s architecture and infrastructure.

Since modules are interchangeable, you have more control over Medusa’s architecture. For example, you can choose to use Memcached for event handling instead of Redis.

***

## Infrastructure Module Types

There are different Infrastructure Module types including:

![Diagram illustrating how the modules connect to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1759762284/Medusa%20Book/service-infra_k3fcy0.jpg)

- [Analytics Module](https://docs.medusajs.com/resources/infrastructure-modules/analytics/index.html.md): Integrates a third-party service to track and analyze user interactions and system events.
- [Caching Module](https://docs.medusajs.com/resources/infrastructure-modules/caching/index.html.md): Defines the caching mechanism or logic to cache computational results.
- [Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/index.html.md): Integrates a pub/sub service to handle subscribing to and emitting events.
- [Workflow Engine Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/index.html.md): Integrates a service to store and track workflow executions and steps.
- [File Module](https://docs.medusajs.com/resources/infrastructure-modules/file/index.html.md): Integrates a storage service to handle uploading and managing files.
- [Notification Module](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md): Integrates a third-party service or defines custom logic to send notifications to users and customers.
- [Locking Module](https://docs.medusajs.com/resources/infrastructure-modules/locking/index.html.md): Integrates a service that manages access to shared resources by multiple processes or threads.

The Caching Module was introduced in [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0) to replace the deprecated Cache Module.

***

## Infrastructure Modules List

Refer to the [Infrastructure Modules reference](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) for a list of Medusa’s Infrastructure Modules, available modules to install, and how to create an Infrastructure Module.


# Module Isolation

In this chapter, you'll learn how modules are isolated, and what that means for your custom development.

- Modules can't access resources, such as services or data models, from other modules.
- Use [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) to extend an existing module's data models, and [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) to retrieve data across modules.
- Use [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) to build features that depend on functionalities from different modules.

## How are Modules Isolated?

A module is unaware of any resources other than its own, such as services or data models. This means it can't access these resources if they're implemented in another module.

For example, your custom module can't resolve the Product Module's main service or have direct relationships from its data model to the Product Module's data models.

A module has its own container, as explained in the [Module Container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md) chapter. This container includes the module's resources, such as services and data models, and some Framework resources that the Medusa application provides.

Refer to the [Module Container Resources](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md) for a list of resources registered in a module's container.

***

## Why are Modules Isolated

Some of the module isolation's benefits include:

- Integrate your module into any Medusa application without side-effects to your setup.
- Replace existing modules with your custom implementation if your use case is drastically different.
- Use modules in other environments, such as Edge functions and Next.js apps.

***

## How to Extend Data Model of Another Module?

To extend the data model of another module, such as the `Product` data model of the Product Module, use [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). Module Links allow you to build associations between data models of different modules without breaking the module isolation.

Then, you can retrieve data across modules using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md).

***

## How to Use Services of Other Modules?

You'll often build feature that uses functionalities from different modules. For example, if you may need to retrieve brands, then sync them to a third-party service.

To build functionalities spanning across modules and systems, create a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) whose steps resolve the modules' services to perform these functionalities.

Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output.

### Example

For example, consider you have two modules:

1. A module that stores and manages brands in your application.
2. A module that integrates a third-party Content Management System (CMS).

To sync brands from your application to the third-party system, create the following steps:

```ts title="Example Steps" highlights={stepsHighlights}
const retrieveBrandsStep = createStep(
  "retrieve-brands",
  async (_, { container }) => {
    const brandModuleService = container.resolve(
      "brand"
    )

    const brands = await brandModuleService.listBrands()

    return new StepResponse(brands)
  }
)

const createBrandsInCmsStep = createStep(
  "create-brands-in-cms",
  async ({ brands }, { container }) => {
    const cmsModuleService = container.resolve(
      "cms"
    )

    const cmsBrands = await cmsModuleService.createBrands(brands)

    return new StepResponse(cmsBrands, cmsBrands)
  },
  async (brands, { container }) => {
    const cmsModuleService = container.resolve(
      "cms"
    )

    await cmsModuleService.deleteBrands(
      brands.map((brand) => brand.id)
    )
  }
)
```

The `retrieveBrandsStep` retrieves the brands from a Brand Module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS Module.

Then, create the following workflow that uses these steps:

```ts title="Example Workflow"
export const syncBrandsWorkflow = createWorkflow(
  "sync-brands",
  () => {
    const brands = retrieveBrandsStep()

    createBrandsInCmsStep({ brands })
  }
)
```

You can then use this workflow in an API route, scheduled job, or other resources that use this functionality.

***

## How to Use Framework APIs and Tools in Module?

### Framework Tools in Module Container

A module has in its container some Framework APIs and tools, such as [Logger](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md). You can refer to the [Module Container Resources](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md) for a list of resources registered in a module's container.

You can resolve those resources in the module's services and loaders.

For example:

```ts title="Example Service"
import { Logger } from "@medusajs/framework/types"

type InjectedDependencies = {
  logger: Logger
}

export default class BlogModuleService {
  protected logger_: Logger

  constructor({ logger }: InjectedDependencies) {
    this.logger_ = logger

    this.logger_.info("[BlogModuleService]: Hello World!")
  }

  // ...
}
```

In this example, the `BlogModuleService` class resolves the `Logger` service from the module's container and uses it to log a message.

### Using Framework Tools in Workflows

Some Framework APIs and tools are not registered in the module's container. For example, [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) is only registered in the Medusa container.

You should, instead, build workflows that use these APIs and tools along with your module's service.

For example, you can create a workflow that retrieves data using Query, then pass the data to your module's service to perform some action.

```ts title="Example Workflow"
import { createWorkflow, createStep } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

const createBrandsInCmsStep = createStep(
  "create-brands-in-cms",
  async ({ brands }, { container }) => {
    const cmsModuleService = container.resolve(
      "cms"
    )

    const cmsBrands = await cmsModuleService.createBrands(brands)

    return new StepResponse(cmsBrands, cmsBrands)
  },
  async (brands, { container }) => {
    const cmsModuleService = container.resolve(
      "cms"
    )

    await cmsModuleService.deleteBrands(
      brands.map((brand) => brand.id)
    )
  }
)

const syncBrandsWorkflow = createWorkflow(
  "sync-brands",
  () => {
    const { data: brands } = useQueryGraphStep({
      entity: "brand",
      fields: [
        "*",
        "products.*",
      ],
    })

    createBrandsInCmsStep({ brands })
  }
)
```

In this example, you use the `useQueryGraphStep` to retrieve brands with their products, then pass the brands to the `createBrandsInCmsStep` step.

In the `createBrandsInCmsStep`, you resolve the CMS Module's service from the module's container and use it to create the brands in the third-party system. You pass the brands you retrieved using Query to the module's service.

### Injecting Dependencies to Module

Some cases still require you to access external resources, mainly [Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) or Framework tools, in your module.
For example, you may need the [Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/index.html.md) to emit events from your module's service.

In those cases, you can inject the dependencies to your module's service in `medusa-config.ts` using the `dependencies` property of the module's configuration.

Use this approach only when absolutely necessary, where workflows aren't sufficient for your use case. By injecting dependencies, you risk breaking your module if the dependency isn't provided, or if the dependency's API changes.

For example:

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/blog",
      dependencies: [
        Modules.EVENT_BUS,
      ],
    },
  ],
})
```

In this example, you inject the Event Module's service to your module's container.

Only the main service will be injected into the module's container.

You can then use the Event Module's service in your module's service:

```ts title="Example Service"
class BlogModuleService {
  protected eventBusService_: AbstractEventBusModuleService

  constructor({ event_bus }) {
    this.eventBusService_ = event_bus
  }

  performAction() {
    // TODO perform action

    this.eventBusService_.emit({
      name: "custom.event",
      data: {
        id: "123",
        // other data payload
      },
    })
  }
}
```


# Loaders

In this chapter, you’ll learn about loaders and how to use them.

## What is a Loader?

When building a commerce application, you'll often need to execute an action the first time the application starts. For example, if your application needs to connect to databases other than Medusa's PostgreSQL database, you might need to establish a connection on application startup.

In Medusa, you can execute an action when the application starts using a loader. A loader is a function exported by a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), which is a package of business logic for a single domain. When the Medusa application starts, it executes all loaders exported by configured modules.

Loaders are useful to register custom resources, such as database connections, in the [module's container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md), which is similar to the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) but includes only [resources available to the module](https://docs.medusajs.com/resources/medusa-container-resources#module-container-resources/index.html.md). Modules are isolated, so they can't access resources outside of them, such as a service in another module.

Medusa isolates modules to ensure that they're re-usable across applications, aren't tightly coupled to other resources, and don't have implications when integrated into the Medusa application. Learn more about why modules are isolated in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), and check out [this reference for the list of resources in the module's container](https://docs.medusajs.com/resources/medusa-container-resources#module-container-resources/index.html.md).

***

## How to Create a Loader?

### 1. Implement Loader Function

You create a loader function in a TypeScript or JavaScript file under a module's `loaders` directory.

For example, consider you have a `hello` module, you can create a loader at `src/modules/hello/loaders/hello-world.ts` with the following content:

![Example of loader file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732865671/Medusa%20Book/loader-dir-overview_eg6vtu.jpg)

Learn how to create a module in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md).

```ts title="src/modules/hello/loaders/hello-world.ts"
import {
  LoaderOptions,
} from "@medusajs/framework/types"

export default async function helloWorldLoader({
  container,
}: LoaderOptions) {
  const logger = container.resolve("logger")

  logger.info("[HELLO MODULE] Just started the Medusa application!")
}
```

The loader file exports an async function, which is the function executed when the application loads.

The function receives an object parameter that has a `container` property, which is the module's container that you can use to resolve resources from. In this example, you resolve the Logger utility to log a message in the terminal.

Find the list of resources in the module's container in [this reference](https://docs.medusajs.com/resources/medusa-container-resources#module-container-resources/index.html.md).

### 2. Export Loader in Module Definition

After implementing the loader, you must export it in the module's definition in the `index.ts` file at the root of the module's directory. Otherwise, the Medusa application will not run it.

So, to export the loader you implemented above in the `hello` module, add the following to `src/modules/hello/index.ts`:

```ts title="src/modules/hello/index.ts"
// other imports...
import helloWorldLoader from "./loaders/hello-world"

export default Module("hello", {
  // ...
  loaders: [helloWorldLoader],
})
```

The second parameter of the `Module` function accepts a `loaders` property whose value is an array of loader functions. The Medusa application will execute these functions when it starts.

### Test the Loader

Assuming your module is [added to Medusa's configuration](https://docs.medusajs.com/learn/fundamentals/modules#4-add-module-to-medusas-configurations/index.html.md), you can test the loader by starting the Medusa application:

```bash npm2yarn
npm run dev
```

Then, you'll find the following message logged in the terminal:

```plain
info:   [HELLO MODULE] Just started the Medusa application!
```

This indicates that the loader in the `hello` module ran and logged this message.

***

## When are Loaders Executed?

### Loaders Executed on Application Startup

When you start the Medusa application, it executes the loaders of all modules in their registration order.

A loader is executed before the module's main service is instantiated. So, you can use loaders to register in the module's container resources that you want to use in the module's service. For example, you can register a database connection.

Loaders are also useful to only load a module if a certain condition is met. For example, if you try to connect to a database in a loader but the connection fails, you can throw an error in the loader to prevent the module from being loaded. This is useful if your module depends on an external service to work.

### Loaders Executed with Migrations

Loaders are also executed when you run [migrations](https://docs.medusajs.com/learn/fundamentals/data-models/write-migration/index.html.md). This can be useful if you need to run some task before the migrations, or you want to migrate some data to an integrated third-party system as part of the migration process.

***

## Avoid Heavy Operations in Loaders

Since loaders are executed when the Medusa application starts, heavy operations will increase the startup time of the application.

So, avoid operations that take a long time to complete, such as fetching a large amount of data from an external API or database, in loaders.

### Alternative Solutions

Instead of performing heavy operations in loaders, consider one of the following solutions:

- Use a [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) to perform the operation at specified intervals. This way, the operation is performed asynchronously and doesn't block the application startup.
- [Emit custom events](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/emit-event/index.html.md) in an [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), then handle the event in a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) to perform the operation asynchronously. You can then send a request to the API route to trigger the operation when needed.

***

## Example: Register Custom MongoDB Connection

As mentioned in this chapter's introduction, loaders are most useful when you need to register a custom resource in the module's container to re-use it in other customizations in the module.

Consider your have a MongoDB module that allows you to perform operations on a MongoDB database.

### Prerequisites

- [MongoDB database that you can connect to from a local machine.](https://www.mongodb.com)
- [Install the MongoDB SDK in your Medusa application.](https://www.mongodb.com/docs/drivers/node/current/quick-start/download-and-install/#install-the-node.js-driver)

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 "@medusajs/framework/awilix"
import { MongoClient } from "mongodb"

type ModuleOptions = {
  connection_url?: string
  db_name?: string
}

export default async function mongoConnectionLoader({
  container,
  options,
}: LoaderOptions<ModuleOptions>) {
  if (!options.connection_url) {
    throw new Error(`[MONGO MDOULE]: connection_url option is required.`)
  }
  if (!options.db_name) {
    throw new Error(`[MONGO MDOULE]: db_name option is required.`)
  }
  const logger = container.resolve("logger")
  
  try {
    const clientDb = (
      await (new MongoClient(options.connection_url)).connect()
    ).db(options.db_name)

    logger.info("Connected to MongoDB")

    container.register(
      "mongoClient",
      asValue(clientDb)
    )
  } catch (e) {
    logger.error(
      `[MONGO MDOULE]: An error occurred while connecting to MongoDB: ${e}`
    )
  }
}
```

The loader function accepts in its object parameter an `options` property, which is the options passed to the module in Medusa's configurations. For example:

```ts title="medusa-config.ts" highlights={optionHighlights}
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/mongo",
      options: {
        connection_url: process.env.MONGO_CONNECTION_URL,
        db_name: process.env.MONGO_DB_NAME,
      },
    },
  ],
})
```

Passing options is useful when your module needs informations like connection URLs or API keys, as it ensures your module can be re-usable across applications. For the MongoDB Module, you expect two options:

- `connection_url`: the URL to connect to the MongoDB database.
- `db_name`: The name of the database to connect to.

In the loader, you check first that these options are set before proceeding. Then, you create an instance of the MongoDB client and connect to the database specified in the options.

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 [`@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

After registering the custom MongoDB client in the module's container, you can now resolve and use it in the module's service.

For example:

```ts title="src/modules/mongo/service.ts"
import type { Db } from "mongodb"

type InjectedDependencies = {
  mongoClient: Db
}

export default class MongoModuleService {
  private mongoClient_: Db

  constructor({ mongoClient }: InjectedDependencies) {
    this.mongoClient_ = mongoClient
  }

  async createMovie({ title }: {
    title: string
  }) {
    const moviesCol = this.mongoClient_.collection("movie")

    const insertedMovie = await moviesCol.insertOne({
      title,
    })

    const movie = await moviesCol.findOne({
      _id: insertedMovie.insertedId,
    })

    return movie
  }

  async deleteMovie(id: string) {
    const moviesCol = this.mongoClient_.collection("movie")

    await moviesCol.deleteOne({
      _id: {
        equals: id,
      },
    })
  }
}
```

The service `MongoModuleService` resolves the `mongoClient` resource you registered in the loader and sets it as a class property. You then use it in the `createMovie` and `deleteMovie` methods, which create and delete a document in a `movie` collection in the MongoDB database, respectively.

Make sure to export the loader in the module's definition in the `index.ts` file at the root directory of the module:

```ts title="src/modules/mongo/index.ts" highlights={[["9"]]}
import { Module } from "@medusajs/framework/utils"
import MongoModuleService from "./service"
import mongoConnectionLoader from "./loaders/connection"

export const MONGO_MODULE = "mongo"

export default Module(MONGO_MODULE, {
  service: MongoModuleService,
  loaders: [mongoConnectionLoader],
})
```

### Test it Out

You can test the connection out by starting the Medusa application. If it's successful, you'll see the following message logged in the terminal:

```bash
info:    Connected to MongoDB
```

You can now resolve the MongoDB Module's main service in your customizations to perform operations on the MongoDB database.


# Modules Directory Structure

In this chapter, you'll learn about the expected files and directories in your module.

![Module Directory Structure Example](https://res.cloudinary.com/dza7lstvk/image/upload/v1714379976/Medusa%20Book/modules-dir-overview_nqq7ne.jpg)

## index.ts

The `index.ts` file is in the root of your module's directory and exports the module's definition as explained in the [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) chapter.

***

## service.ts

A module must have a main service that contains the module's business logic. It's created in the `service.ts` file at the root of your module directory as explained in the [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) chapter.

***

## Other Directories

The following directories are optional and you can choose to create them based on your module's functionality:

- `models`: Holds the [data models](https://docs.medusajs.com/learn/fundamentals/data-models/index.html.md) representing tables in the database.
- `migrations`: Holds the [migration](https://docs.medusajs.com/learn/fundamentals/data-models/write-migration/index.html.md) files used to reflect changes on the database.
- `loaders`: Holds the [script files to run on the application's startup](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md) when Medusa loads the module.


# Multiple Services in a Module

In this chapter, you'll learn how to use multiple services in a module.

## Module's Main and Internal Services

A module has one main service only, which is the service exported in the module's definition.

However, you may use other services in your module to better organize your code or split functionalities. These are called internal services that can be resolved within your module, but not in external resources.

***

## How to Add an Internal Service

### 1. Create Service

To add an internal service, create it in the `services` directory of your module.

For example, create the file `src/modules/blog/services/client.ts` with the following content:

```ts title="src/modules/blog/services/client.ts"
export class ClientService {
  async getMessage(): Promise<string> {
    return "Hello, World!"
  }
}
```

### 2. Export Service in Index

Next, create an `index.ts` file under the `services` directory of the module that exports your internal services.

For example, create the file `src/modules/blog/services/index.ts` with the following content:

```ts title="src/modules/blog/services/index.ts"
export * from "./client"
```

This exports the `ClientService`.

### 3. Resolve Internal Service

Internal services exported in the `services/index.ts` file of your module are now registered in the container and can be resolved in other services in the module as well as loaders.

For example, in your main service:

```ts title="src/modules/blog/service.ts" highlights={[["5"], ["13"]]}
// other imports...
import { ClientService } from "./services"

type InjectedDependencies = {
  clientService: ClientService
}

class BlogModuleService extends MedusaService({
  Post,
}){
  protected clientService_: ClientService

  constructor({ clientService }: InjectedDependencies) {
    super(...arguments)
    this.clientService_ = clientService
  }
}
```

You can now use your internal service in your main service.

***

## Resolve Resources in Internal Service

Resolve dependencies from your module's container in the constructor of your internal service.

For example:

```ts
import { Logger } from "@medusajs/framework/types"

type InjectedDependencies = {
  logger: Logger
}

export class ClientService {
  protected logger_: Logger

  constructor({ logger }: InjectedDependencies) {
    this.logger_ = logger
  }
}
```

***

## Access Module Options

Your internal service can't access the module's options.

To retrieve the module's options, use the `configModule` registered in the module's container, which is the configurations in `medusa-config.ts`.

For example:

```ts
import { ConfigModule } from "@medusajs/framework/types"
import { BLOG_MODULE } from ".."

export type InjectedDependencies = {
  configModule: ConfigModule
}

export class ClientService {
  protected options: Record<string, any>

  constructor({ configModule }: InjectedDependencies) {
    const moduleDef = configModule.modules[BLOG_MODULE]

    if (typeof moduleDef !== "boolean") {
      this.options = moduleDef.options
    }
  }
}
```

The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key.

If its value is not a `boolean`, set the service's options to the module configuration's `options` property.


# Module Options

In this chapter, you’ll learn about passing options to your module from the Medusa application’s configurations and using them in the module’s resources.

## What are Module Options?

A module can receive options to customize or configure its functionality. For example, if you’re creating a module that integrates a third-party service, you’ll want to receive the integration credentials in the options rather than adding them directly in your code.

***

## How to Pass Options to a Module?

To pass options to a module, add an `options` property to the module’s configuration in `medusa-config.ts`.

For example:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/blog",
      options: {
        capitalize: true,
      },
    },
  ],
})
```

The `options` property’s value is an object. You can pass any properties you want.

### Pass Options to a Module in a Plugin

If your module is part of a plugin, you can pass options to the module in the plugin’s configuration.

For example:

```ts title="medusa-config.ts"
import { defineConfig } from "@medusajs/framework/utils"
module.exports = defineConfig({
  plugins: [
    {
      resolve: "@myorg/plugin-name",
      options: {
        capitalize: true,
      },
    },
  ],
})
```

The `options` property in the plugin configuration is passed to all modules in a plugin.

***

## Access Module Options in Main Service

The module’s main service receives the module options as a second parameter.

For example:

```ts title="src/modules/blog/service.ts" highlights={[["12"], ["14", "options?: ModuleOptions"], ["17"], ["18"], ["19"]]}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

// recommended to define type in another file
type ModuleOptions = {
  capitalize?: boolean
}

export default class BlogModuleService extends MedusaService({
  Post,
}){
  protected options_: ModuleOptions

  constructor({}, options?: ModuleOptions) {
    super(...arguments)

    this.options_ = options || {
      capitalize: false,
    }
  }

  // ...
}
```

***

## Access Module Options in Loader

The object that a module’s loaders receive as a parameter has an `options` property holding the module's options.

For example:

```ts title="src/modules/blog/loaders/hello-world.ts" highlights={[["11"], ["12", "ModuleOptions", "The type of expected module options."], ["16"]]}
import {
  LoaderOptions,
} from "@medusajs/framework/types"

// recommended to define type in another file
type ModuleOptions = {
  capitalize?: boolean
}

export default async function helloWorldLoader({
  options,
}: LoaderOptions<ModuleOptions>) {
  
  console.log(
    "[BLOG MODULE] Just started the Medusa application!",
    options
  )
}
```

***

## Validate Module Options

If you expect a certain option and want to throw an error if it's not provided or isn't valid, it's recommended to perform the validation in a loader. The module's service is only instantiated when it's used, whereas the loader runs when the Medusa application starts.

So, by performing the validation in the loader, you ensure you can throw an error at an early point, rather than when the module is used.

For example, to validate that the Hello Module received an `apiKey` option, create the loader `src/modules/loaders/validate.ts`:

```ts title="src/modules/blog/loaders/validate.ts" 
import { LoaderOptions } from "@medusajs/framework/types"
import { MedusaError } from "@medusajs/framework/utils"

// recommended to define type in another file
type ModuleOptions = {
  apiKey?: string
}

export default async function validationLoader({
  options,
}: LoaderOptions<ModuleOptions>) {
  if (!options.apiKey) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "Hello Module requires an apiKey option."
    )
  }
}
```

Then, export the loader in the module's definition file, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md):

```ts title="src/modules/blog/index.ts"
// other imports...
import validationLoader from "./loaders/validate"

export const BLOG_MODULE = "blog"

export default Module(BLOG_MODULE, {
  // ...
  loaders: [validationLoader],
})
```

Now, when the Medusa application starts, the loader will run, validating the module's options and throwing an error if the `apiKey` option is missing.


# Modules

In this chapter, you’ll learn about modules and how to create them.

## What is a Module?

A module is a reusable package of functionalities related to a single domain or integration. Medusa comes with multiple pre-built modules for core commerce needs, such as the [Cart Module](https://docs.medusajs.com/resources/commerce-modules/cart/index.html.md) that holds the data models and business logic for cart operations.

When building a commerce application, you often need to introduce custom behavior specific to your products, tech stack, or your general ways of working. In other commerce platforms, introducing custom business logic and data models requires setting up separate applications to manage these customizations.

Medusa removes this overhead by allowing you to easily write custom modules that integrate into the Medusa application without affecting the existing setup. You can also re-use your modules across Medusa projects.

As you learn more about Medusa, you will see that modules are central to customizations and integrations. With modules, your Medusa application can turn into a middleware solution for your commerce ecosystem.

- You want to build a custom feature related to a single domain or integrate a third-party service.

- You want to create a reusable package of customizations that include not only modules, but also API routes, workflows, and other customizations. Instead, use a [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md).

***

## How to Create a Module?

In a module, you define data models that represent new tables in the database, and you manage these models in a class called a service. Then, the Medusa application registers the module's service in the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) so that you can build commerce flows and features around the functionalities provided by the module.

In this section, you'll build a Blog Module that has a `Post` data model and a service to manage that data model. You'll also expose an API endpoint to create a blog post.

Modules are created in a sub-directory of `src/modules`. So, start by creating the directory `src/modules/blog`.

### 1. Create Data Model

A data model represents a table in the database. You create data models using Medusa's data modeling language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

You create a data model in a TypeScript or JavaScript file under the `models` directory of a module. So, to create a `Post` data model in the Blog Module, create the file `src/modules/blog/models/post.ts` with the following content:

![Blog module directory structure showing the organized file hierarchy with the newly created 'models' folder containing the 'post.ts' data model file, which defines the data model for blog posts using Medusa's data modeling language](https://res.cloudinary.com/dza7lstvk/image/upload/v1732806790/Medusa%20Book/blog-dir-overview-1_jfvovj.jpg)

```ts title="src/modules/blog/models/post.ts"
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id().primaryKey(),
  title: model.text(),
})

export default Post
```

You define the data model using the `define` method of the DML. It accepts two parameters:

1. The first one is the name of the data model's table in the database. Use snake-case names.
2. The second is an object, which is the data model's schema. The schema's properties are defined using the `model`'s methods, such as `text` and `id`.
   - Data models automatically have the date properties `created_at`, `updated_at`, and `deleted_at`, so you don't need to add them manually.

Learn about other property types in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties#property-types/index.html.md).

The code snippet above defines a `Post` data model with `id` and `title` properties.

### 2. Create Service

You perform database operations on your data models in a service, which is a class exported by the module and acts like an interface to its functionalities. Medusa registers the service in its [container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md), allowing you to resolve and use it when building custom commerce flows.

You define a service in a `service.ts` or `service.js` file at the root of your module's directory. So, to create the Blog Module's service, create the file `src/modules/blog/service.ts` with the following content:

![Updated directory overview after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1732807230/Medusa%20Book/blog-dir-overview-2_avzb9l.jpg)

```ts title="src/modules/blog/service.ts" highlights={highlights}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

class BlogModuleService extends MedusaService({
  Post,
}){
}

export default BlogModuleService
```

Your module's service extends a class generated by `MedusaService` from the Modules SDK. This class comes with generated methods for data-management Create, Read, Update, and Delete (CRUD) operations on each of your modules, saving you time that can be spent on building custom business logic.

The `MedusaService` function accepts an object of data models to generate methods for. You can pass all data models in your module in this object.

For example, the `BlogModuleService` now has a `createPosts` method to create post records, and a `retrievePost` method to retrieve a post record. The suffix of each method (except for `retrieve`) is the pluralized name of the data model.

Find all methods generated by the `MedusaService` in [this reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md)

If a module doesn't have data models, such as when it's integrating a third-party service, it doesn't need to extend `MedusaService`.

### 3. Export Module Definition

The final piece to a module is its definition, which is exported in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its main service. Medusa will then register the main service in the container under the module's name.

So, to export the definition of the Blog Module, create the file `src/modules/blog/index.ts` with the following content:

![Updated directory overview after adding the module definition](https://res.cloudinary.com/dza7lstvk/image/upload/v1732808511/Medusa%20Book/blog-dir-overview-3_dcgjaa.jpg)

```ts title="src/modules/blog/index.ts" highlights={moduleDefinitionHighlights}
import BlogModuleService from "./service"
import { Module } from "@medusajs/framework/utils"

export const BLOG_MODULE = "blog"

export default Module(BLOG_MODULE, {
  service: BlogModuleService,
})
```

You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters:

1. The name that the module's main service is registered under (`blog`). The module name can contain only alphanumeric characters and underscores.
2. An object with a required property `service` indicating the module's main service.

You export `BLOG_MODULE` to reference the module's name more reliably when resolving its service in other customizations.

### 4. Add Module to Medusa's Configurations

If you're creating the module in a plugin, this step isn't required as the module is registered when the plugin is registered. Learn more about plugins in [this documentation](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md).

Once you finish building the module, add it to Medusa's configurations to start using it. Medusa will then register the module's main service in the Medusa container, allowing you to resolve and use it in other customizations.

In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:

```ts title="medusa-config.ts" highlights={[["7"]]}
module.exports = defineConfig({
  projectConfig: {
    // ...
  },
  modules: [
    {
      resolve: "./src/modules/blog",
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

### 5. Generate Migrations

Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module.

Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations.

You don't have to write migrations yourself. Medusa's CLI tool has a command that generates the migrations for you. You can also use this command again when you make changes to the module at a later point, and it will generate new migrations for that change.

To generate a migration for the Blog Module, run the following command in your Medusa application's directory:

If you're creating the module in a plugin, use the [plugin:db:generate command](https://docs.medusajs.com/resources/medusa-cli/commands/plugin#plugindbgenerate/index.html.md) instead.

```bash
npx medusa db:generate blog
```

The `db:generate` command of the Medusa CLI accepts one or more module names to generate the migration for. It will create a migration file for the Blog Module in the directory `src/modules/blog/migrations` similar to the following:

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 "@medusajs/framework/mikro-orm/migrations"

export class Migration20241121103722 extends Migration {

  async up(): Promise<void> {
    this.addSql("create table if not exists \"post\" (\"id\" text not null, \"title\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"post_pkey\" primary key (\"id\"));")
  }

  async down(): Promise<void> {
    this.addSql("drop table if exists \"post\" cascade;")
  }

}
```

In the migration class, the `up` method creates the table `post` and defines its columns using PostgreSQL syntax. The `down` method drops the table.

### 6. Run Migrations

To reflect the changes in the generated migration file on the database, run the `db:migrate` command:

If you're creating the module in a plugin, run this command on the Medusa application that the plugin is installed in.

```bash
npx medusa db:migrate
```

This creates the `post` table in the database.

***

## Test the Module

Since the module's main service is registered in the Medusa container, you can resolve it in other customizations to use its methods.

To test out the Blog Module, you'll add the functionality to create a post in a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), which is a special function that performs a task in a series of steps with rollback logic. Then, you'll expose an [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) that creates a blog post by executing the workflow.

By building a commerce feature in a workflow, you can execute it in other customizations while ensuring data consistency across systems. If an error occurs during execution, every step has its own rollback logic to undo its actions. Workflows have other special features which you can learn about in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md).

To create the workflow, create the file `src/workflows/create-post.ts` with the following content:

```ts title="src/workflows/create-post.ts" highlights={workflowHighlights}
import { 
  createStep, 
  createWorkflow, 
  StepResponse, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { BLOG_MODULE } from "../modules/blog"
import BlogModuleService from "../modules/blog/service"

type CreatePostWorkflowInput = {
  title: string
}

const createPostStep = createStep(
  "create-post",
  async ({ title }: CreatePostWorkflowInput, { container }) => {
    const blogModuleService: BlogModuleService = container.resolve(BLOG_MODULE)

    const post = await blogModuleService.createPosts({
      title,
    })

    return new StepResponse(post, post)
  },
  async (post, { container }) => {
    if (!post) {
      return
    }
    const blogModuleService: BlogModuleService = container.resolve(BLOG_MODULE)

    await blogModuleService.deletePosts(post.id)
  }
)

export const createPostWorkflow = createWorkflow(
  "create-post",
  (postInput: CreatePostWorkflowInput) => {
    const post = createPostStep(postInput)

    return new WorkflowResponse(post)
  }
)
```

The workflow has a single step `createPostStep` that creates a post. In the step, you resolve the Blog Module's service from the Medusa container, which the step receives as a parameter. Then, you create the post using the method `createPosts` of the service, which was generated by `MedusaService`.

The step also has a compensation function, which is a function passed as a third-parameter to `createStep` that implements the logic to rollback the change made by a step in case an error occurs during the workflow's execution.

You'll now execute that workflow in an API route to expose the feature of creating blog posts to clients. To create an API route, create the file `src/api/blog/posts/route.ts` with the following content:

```ts title="src/api/blog/posts/route.ts"
import type { 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { 
  createPostWorkflow,
} from "../../../workflows/create-post"

export async function POST(
  req: MedusaRequest, 
  res: MedusaResponse
) {
  const { result: post } = await createPostWorkflow(req.scope)
    .run({
      input: {
        title: "My Post",
      },
    })

  res.json({
    post,
  })
}
```

This adds a `POST` API route at `/blog/posts`. In the API route, you execute the `createPostWorkflow` by invoking it, passing it the Medusa container in `req.scope`, then invoking the `run` method. In the `run` method, you pass the workflow's input in the `input` property.

To test this out, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, send a `POST` request to `/blog/posts`:

```bash
curl -X POST http://localhost:9000/blog/posts
```

This will create a post and return it in the response:

```json
{
  "post": {
    "id": "123...",
    "title": "My Post",
    "created_at": "...",
    "updated_at": "..."
  }
}
```

You can also execute the workflow from a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) when an event occurs, or from a [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) to run it at a specified interval.


# Service Constraints

This chapter lists constraints to keep in mind when creating a service.

## Use Async Methods

Medusa wraps service method executions to inject useful context or transactions. However, since Medusa can't detect whether the method is asynchronous, it always executes methods in the wrapper with the `await` keyword.

For example, if you have a synchronous `getMessage` method, and you use it in other resources like workflows, Medusa executes it as an async method:

```ts
await blogModuleService.getMessage()
```

So, make sure your service's methods are always async to avoid unexpected errors or behavior.

```ts highlights={[["8", "", "Method must be async."], ["13", "async", "Correct way of defining the method."]]}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

class BlogModuleService extends MedusaService({
  Post,
}){
  // Don't
  getMessage(): string {
    return "Hello, World!"
  }

  // Do
  async getMessage(): Promise<string> {
    return "Hello, World!"
  }
}

export default BlogModuleService
```


# Service Factory

In this chapter, you’ll learn about what the service factory is and how to use it.

## What is the Service Factory?

Medusa provides a service factory that your module’s main service can extend.

The service factory generates data management methods for your data models, saving you time on implementing these methods manually.

Your service provides data-management functionality for your data models.

***

## How to Extend the Service Factory?

Medusa provides the service factory as a `MedusaService` function your service extends. The function creates and returns a service class with generated data-management methods.

For example, create the file `src/modules/blog/service.ts` with the following content:

```ts title="src/modules/blog/service.ts" highlights={highlights}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

class BlogModuleService extends MedusaService({
  Post,
}){
  // TODO implement custom methods
}

export default BlogModuleService
```

### MedusaService Parameters

The `MedusaService` function accepts one parameter, which is an object of data models for which to generate data-management methods.

In the example above, since the `BlogModuleService` extends `MedusaService`, it has methods to manage the `Post` data model, such as `createPosts`.

### Generated Methods

The service factory generates methods to manage the records of each of the data models provided in the first parameter in the database.

The method names are the operation name, suffixed by the data model's key in the object parameter passed to `MedusaService`.

For example, the following methods are generated for the service above:

Find a complete reference of each of the methods in [this documentation](https://docs.medusajs.com/resources/service-factory-reference/index.html.md)

### listPosts

### listPosts

This method retrieves an array of records based on filters and pagination configurations.

For example:

```ts
const posts = await blogModuleService
  .listPosts()

// with filters
const posts = await blogModuleService
  .listPosts({
    id: ["123"]
  })
```

### listAndCountPosts

### retrievePost

This method retrieves a record by its ID.

For example:

```ts
const post = await blogModuleService
  .retrievePost("123")
```

### retrievePost

### updatePosts

This method updates and retrieves records of the data model.

For example:

```ts
const post = await blogModuleService
  .updatePosts({
    id: "123",
    title: "test"
  })

// update multiple
const posts = await blogModuleService
  .updatePosts([
    {
      id: "123",
      title: "test"
    },
    {
      id: "321",
      title: "test 2"
    },
  ])

// use filters
const posts = await blogModuleService
  .updatePosts([
    {
      selector: {
        id: ["123", "321"]
      },
      data: {
        title: "test"
      }
    },
  ])
```

### createPosts

### softDeletePosts

This method soft-deletes records using an array of IDs or an object of filters.

For example:

```ts
await blogModuleService.softDeletePosts("123")

// soft-delete multiple
await blogModuleService.softDeletePosts([
  "123", "321"
])

// use filters
await blogModuleService.softDeletePosts({
  id: ["123", "321"]
})
```

### updatePosts

### deletePosts

### softDeletePosts

### restorePosts

### Using a Constructor

If you implement a `constructor` in your service, make sure to call `super` and pass it `...arguments`.

For example:

```ts highlights={[["8"]]}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

class BlogModuleService extends MedusaService({
  Post,
}){
  constructor() {
    super(...arguments)
  }
}

export default BlogModuleService
```

***

## Generated Internal Services

The service factory also generates internal services for each data model passed to the `MedusaService` function. These services are registered in the module's container and can be resolved using their camel-cased names.

For example, if the `BlogModuleService` is defined as follows:

```ts
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

class BlogModuleService extends MedusaService({
  Post,
}){
}

export default BlogModuleService
```

Then, you'll have a `postService` registered in the module's container that allows you to manage posts.

Generated internal services have the same methods as the `BlogModuleService`, such as `create`, `retrieve`, `update`, and `delete`, but without the data model name suffix.

These services are useful when you need to perform database operations in loaders, as they are executed before the module's services are registered. Learn more in the [Module Container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md) documentation.

For example, you can create a loader that logs the number of posts in the database:

```ts
import { LoaderOptions } from "@medusajs/framework/types"

export default async function helloWorldLoader({
  container,
}: LoaderOptions) {
  const postService = container.resolve("postService")

  const [_, count] = await postService.listAndCount()

  console.log(`[helloWorldLoader]: There are ${count} posts in the database.`)
}
```


# Create a Plugin

In this chapter, you'll learn how to create a Medusa plugin and publish it.

A [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) is a package of reusable Medusa customizations that you can install in any Medusa application. By creating and publishing a plugin, you can reuse your Medusa customizations across multiple projects or share them with the community.

Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0).

## 1. Create a Plugin Project

Plugins are created in a separate Medusa project. This makes the development and publishing of the plugin easier. Later, you'll install that plugin in your Medusa application to test it out and use it.

Medusa's `create-medusa-app` CLI tool provides the option to create a plugin project. Run the following command to create a new plugin project:

```bash
npx create-medusa-app my-plugin --plugin
```

This will create a new Medusa plugin project in the `my-plugin` directory.

### Plugin Directory Structure

After the installation is done, the plugin structure will look like this:

![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg)

- `src/`: Contains the Medusa customizations.
- `src/admin`: Contains [admin extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md).
- `src/api`: Contains [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) and [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). You can add store, admin, or any custom API routes.
- `src/jobs`: Contains [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md).
- `src/links`: Contains [module links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md).
- `src/modules`: Contains [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md).
- `src/provider`: Contains [module providers](#create-module-providers).
- `src/subscribers`: Contains [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md).
- `src/workflows`: Contains [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). You can also add [hooks](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) under `src/workflows/hooks`.
- `package.json`: Contains the plugin's package information, including general information and dependencies.
- `tsconfig.json`: Contains the TypeScript configuration for the plugin.

***

## 2. Prepare Plugin

### Package Name

Before developing, testing, and publishing your plugin, make sure its name in `package.json` is correct. This is the name you'll use to install the plugin in your Medusa application.

For example:

```json title="package.json"
{
  "name": "@myorg/plugin-name",
  // ...
}
```

### Package Keywords

Medusa scrapes NPM for a list of plugins that integrate third-party services, to later showcase them on the Medusa website. If you want your plugin to appear in that listing, make sure to add the `medusa-v2` and `medusa-plugin-integration` keywords to the `keywords` field in `package.json`.

Only plugins that integrate third-party services are listed in the Medusa integrations page. If your plugin doesn't integrate a third-party service, you can skip this step.

```json title="package.json"
{
  "keywords": [
    "medusa-plugin-integration",
    "medusa-v2"
  ],
  // ...
}
```

In addition, make sure to use one of the following keywords based on your integration type:

|Keyword|Description|Example|
|---|---|---|
|\`medusa-plugin-analytics\`|Analytics service integration|Google Analytics|
|\`medusa-plugin-auth\`|Authentication service integration|Auth0|
|\`medusa-plugin-cms\`|CMS service integration|Contentful|
|\`medusa-plugin-notification\`|Notification service integration|Twilio SMS|
|\`medusa-plugin-payment\`|Payment service integration|PayPal|
|\`medusa-plugin-search\`|Search service integration|MeiliSearch|
|\`medusa-plugin-shipping\`|Shipping service integration|DHL|
|\`medusa-plugin-other\`|Other third-party integrations|Sentry|

### Package Dependencies

Your plugin project will already have the dependencies mentioned in this section. Unless you made changes to the dependencies, you can skip this section.

In the `package.json` file you must have the Medusa dependencies as `devDependencies` and `peerDependencies`. In addition, you must have `@swc/core` as a `devDependency`, as it's used by the plugin CLI tools.

For example, assuming `2.5.0` is the latest Medusa version:

```json title="package.json"
{
  "devDependencies": {
    "@medusajs/admin-sdk": "2.5.0",
    "@medusajs/cli": "2.5.0",
    "@medusajs/framework": "2.5.0",
    "@medusajs/medusa": "2.5.0",
    "@medusajs/test-utils": "2.5.0",
    "@medusajs/ui": "4.0.4",
    "@medusajs/icons": "2.5.0",
    "@swc/core": "1.5.7",
  },
  "peerDependencies": {
    "@medusajs/admin-sdk": "2.5.0",
    "@medusajs/cli": "2.5.0",
    "@medusajs/framework": "2.5.0",
    "@medusajs/test-utils": "2.5.0",
    "@medusajs/medusa": "2.5.0",
    "@medusajs/ui": "4.0.3",
    "@medusajs/icons": "2.5.0",
  }
}
```

### Package Exports

Your plugin project will already have the exports mentioned in this section. Unless you made changes to the exports or you created your plugin before [Medusa v2.7.0](https://github.com/medusajs/medusa/releases/tag/v2.7.0), you can skip this section.

In the `package.json` file, make sure your plugin has the following exports:

```json title="package.json"
{
  "exports": {
    "./package.json": "./package.json",
    "./workflows": "./.medusa/server/src/workflows/index.js",
    "./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js",
    "./providers/*": "./.medusa/server/src/providers/*/index.js",
    "./admin": {
      "import": "./.medusa/server/src/admin/index.mjs",
      "require": "./.medusa/server/src/admin/index.js",
      "default": "./.medusa/server/src/admin/index.js"
    },
    "./*": "./.medusa/server/src/*.js"
  }
}
```

Aside from the `./package.json`, `./providers`, and `./admin`, these exports are only a recommendation. You can cherry-pick the files and directories you want to export.

The plugin exports the following files and directories:

- `./package.json`: The `package.json` file. Medusa needs to access the `package.json` when registering the plugin.
- `./workflows`: The workflows exported in `./src/workflows/index.ts`.
- `./.medusa/server/src/modules/*`: The definition file of modules. This is useful if you create links to the plugin's modules in the Medusa application.
- `./providers/*`: The definition file of module providers. This is useful if your plugin includes a module provider, allowing you to register the plugin's providers in Medusa applications. Learn more in the [Create Module Providers](#create-module-providers) section.
- `./admin`: The admin extensions exported in `./src/admin/index.ts`.
- `./*`: Any other files in the plugin's `src` directory.

***

## 3. Publish Plugin Locally for Development and Testing

Medusa's CLI tool provides commands to simplify developing and testing your plugin in a local Medusa application. You start by publishing your plugin in the local package registry, then install it in your Medusa application. You can then watch for changes in the plugin as you develop it.

### Publish and Install Local Package

### Prerequisites

- [Medusa application installed.](https://docs.medusajs.com/learn/installation/index.html.md)

The first time you create your plugin, you need to publish the package into a local package registry, then install it in your Medusa application. This is a one-time only process.

To publish the plugin to the local registry, run the following command in your plugin project:

```bash title="Plugin project"
npx medusa plugin:publish
```

This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`.

Next, navigate to your Medusa application:

```bash title="Medusa application"
cd ~/path/to/medusa-app
```

Make sure to replace `~/path/to/medusa-app` with the path to your Medusa application.

Then, if your project was created before v2.3.1 of Medusa, make sure to install `yalc` as a development dependency:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm install --save-dev yalc
```

After that, run the following Medusa CLI command to install the plugin:

```bash title="Medusa application"
npx medusa plugin:add @myorg/plugin-name
```

Make sure to replace `@myorg/plugin-name` with the name of your plugin as specified in `package.json`. Your plugin will be installed from the local package registry into your Medusa application.

### Register Plugin in Medusa Application

After installing the plugin, you need to register it in your Medusa application in the configurations defined in `medusa-config.ts`.

Add the plugin to the `plugins` array in the `medusa-config.ts` file:

```ts title="medusa-config.ts" highlights={pluginHighlights}
module.exports = defineConfig({
  // ...
  plugins: [
    {
      resolve: "@myorg/plugin-name",
      options: {},
    },
  ],
})
```

The `plugins` configuration is an array of objects where each object has a `resolve` key whose value is the name of the plugin package.

#### Pass Module Options through Plugin

Each plugin configuration also accepts an `options` property, whose value is an object of options to pass to the plugin's modules.

For example:

```ts title="medusa-config.ts" highlights={pluginOptionsHighlight}
module.exports = defineConfig({
  // ...
  plugins: [
    {
      resolve: "@myorg/plugin-name",
      options: {
        apiKey: true,
      },
    },
  ],
})
```

The `options` property in the plugin configuration is passed to all modules in the plugin. Learn more about module options in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md).

### Watch Plugin Changes During Development

While developing your plugin, you can watch for changes in the plugin and automatically update the plugin in the Medusa application using it. This is the only command you'll continuously need during your plugin development.

To do that, run the following command in your plugin project:

```bash title="Plugin project"
npx medusa plugin:develop
```

This command will:

- Watch for changes in the plugin. Whenever a file is changed, the plugin is automatically built.
- Publish the plugin changes to the local package registry. This will automatically update the plugin in the Medusa application using it. You can also benefit from real-time HMR updates of admin extensions.

### Start Medusa Application

You can start your Medusa application's development server to test out your plugin:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

While your Medusa application is running and the plugin is being watched, you can test your plugin while developing it in the Medusa application.

***

## 4. Create Customizations in the Plugin

You can now build your plugin's customizations. The following guide explains how to build different customizations in your plugin.

- [Create a module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md)
- [Create a module link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md)
- [Create a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md)
- [Add a workflow hook](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md)
- [Create an API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md)
- [Add a subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md)
- [Add a scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md)
- [Add an admin widget](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md)
- [Add an admin UI route](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md)

While building those customizations, you can test them in your Medusa application by [watching the plugin changes](#watch-plugin-changes-during-development) and [starting the Medusa application](#start-medusa-application).

### Generating Migrations for Modules

During your development, you may need to generate migrations for modules in your plugin. To do that, first, add the following environment variables in your plugin project:

```plain title="Plugin project"
DB_USERNAME=postgres
DB_PASSWORD=123...
DB_HOST=localhost
DB_PORT=5432
DB_NAME=db_name
```

You can add these environment variables in a `.env` file in your plugin project. The variables are:

- `DB_USERNAME`: The username of the PostgreSQL user to connect to the database.
- `DB_PASSWORD`: The password of the PostgreSQL user to connect to the database.
- `DB_HOST`: The host of the PostgreSQL database. Typically, it's `localhost` if you're running the database locally.
- `DB_PORT`: The port of the PostgreSQL database. Typically, it's `5432` if you're running the database locally.
- `DB_NAME`: The name of the PostgreSQL database to connect to.

Then, run the following command in your plugin project to generate migrations for the modules in your plugin:

```bash title="Plugin project"
npx medusa plugin:db:generate
```

This command generates migrations for all modules in the plugin.

Finally, run these migrations on the Medusa application that the plugin is installed in using the `db:migrate` command:

```bash title="Medusa application"
npx medusa db:migrate
```

The migrations in your application, including your plugin, will run and update the database.

### Importing Module Resources

In the [Prepare Plugin](#2-prepare-plugin) section, you learned about [exported resources](#package-exports) in your plugin.

These exports allow you to import your plugin resources in your Medusa application, including workflows, links and modules.

For example, to import the plugin's workflow in your Medusa application:

`@myorg/plugin-name` is the plugin package's name.

```ts
import { Workflow1, Workflow2 } from "@myorg/plugin-name/workflows"
import BlogModule from "@myorg/plugin-name/modules/blog"
// import other files created in plugin like ./src/types/blog.ts
import BlogType from "@myorg/plugin-name/types/blog"
```

### Create Module Providers

The [exported resources](#package-exports) also allow you to import module providers in your plugin and register them in the Medusa application's configuration. A module provider is a module that provides the underlying logic or integration related to a commerce or Infrastructure Module.

For example, assuming your plugin has a [Notification Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md) called `my-notification`, you can register it in your Medusa application's configuration like this:

`@myorg/plugin-name` is the plugin package's name.

```ts highlights={[["9"]]} title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          {
            resolve: "@myorg/plugin-name/providers/my-notification",
            id: "my-notification",
            options: {
              channels: ["email"],
              // provider options...
            },
          },
        ],
      },
    },
  ],
})
```

You pass to `resolve` the path to the provider relative to the plugin package. So, in this example, the `my-notification` provider is located in `./src/providers/my-notification/index.ts` of the plugin.

To learn how to create module providers, refer to the following guides:

- [File Module Provider](https://docs.medusajs.com/resources/references/file-provider-module/index.html.md)
- [Notification Module Provider](https://docs.medusajs.com/resources/references/notification-provider-module/index.html.md)
- [Auth Module Provider](https://docs.medusajs.com/resources/references/auth/provider/index.html.md)
- [Payment Module Provider](https://docs.medusajs.com/resources/references/payment/provider/index.html.md)
- [Fulfillment Module Provider](https://docs.medusajs.com/resources/references/fulfillment/provider/index.html.md)
- [Tax Module Provider](https://docs.medusajs.com/resources/references/tax/provider/index.html.md)

***

## 5. Publish Plugin to NPM

Make sure to add the keywords mentioned in the [Package Keywords](#package-keywords) section in your plugin's `package.json` file.

Medusa's CLI tool provides a command that bundles your plugin to be published to npm. Once you're ready to publish your plugin publicly, run the following command in your plugin project:

```bash
npx medusa plugin:build
```

The command will compile an output in the `.medusa/server` directory.

You can now publish the plugin to npm using the [NPM CLI tool](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). Run the following command to publish the plugin to npm:

```bash
npm publish
```

If you haven't logged in before with your NPM account, you'll be asked to log in first. Then, your package is published publicly to be used in any Medusa application.

### Install Public Plugin in Medusa Application

You install a plugin that's published publicly using your package manager. For example:

```bash npm2yarn
npm install @myorg/plugin-name
```

Where `@myorg/plugin-name` is the name of your plugin as published on NPM.

Then, register the plugin in your Medusa application's configurations as explained in [this section](#register-plugin-in-medusa-application).

***

## Update a Published Plugin

To update the Medusa dependencies in a plugin, refer to [this documentation](https://docs.medusajs.com/learn/update#update-plugin-project/index.html.md).

If you've published a plugin and you've made changes to it, you'll have to publish the update to NPM again.

First, run the following command to change the version of the plugin:

```bash
npm version <type>
```

Where `<type>` indicates the type of version update you’re publishing. For example, it can be `major` or `minor`. Refer to the [npm version documentation](https://docs.npmjs.com/cli/v10/commands/npm-version) for more information.

Then, re-run the same commands for publishing a plugin:

```bash
npx medusa plugin:build
npm publish
```

This will publish an updated version of your plugin under a new version.


# Plugins

In this chapter, you'll learn what a plugin is in Medusa.

Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0).

## What is a Plugin?

A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. The supported customizations are [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md), [Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), [Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md), and [Admin Extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md).

Plugins allow you to reuse your Medusa customizations across multiple projects or share them with the community. They can be published to npm and installed in any Medusa project.

![Diagram showcasing a wishlist plugin installed in a Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540762/Medusa%20Book/plugin-diagram_oepiis.jpg)

Learn how to create a wishlist plugin in [this guide](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md).

***

## Plugin vs Module

A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) is an isolated package related to a single domain or functionality, such as product reviews or integrating a Content Management System. A module can't access any resources in the Medusa application that are outside its codebase.

A plugin, on the other hand, can contain multiple Medusa customizations, including modules. Your plugin can define a module, then build flows around it.

For example, in a plugin, you can define a module that integrates a third-party service, then add a workflow that uses the module when a certain event occurs to sync data to that service.

- You want to reuse your Medusa customizations across multiple projects.
- You want to share your Medusa customizations with the community.

- You want to build a custom feature related to a single domain or integrate a third-party service. Instead, use a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). You can wrap that module in a plugin if it's used in other customizations, such as if it has a module link or it's used in a workflow.

***

## How to Create a Plugin?

The next chapter explains how you can create and publish a plugin.


# Scheduled Job Number of Executions

In this chapter, you'll learn how to set a limit on the number of times a scheduled job is executed.

## Default Number of Scheduled Job Executions

By default, a scheduled job is executed whenever it matches its specified pattern. For example, if you set a scheduled job to run every five minutes, it will run every five minutes until you stop the Medusa application.

***

## Configure Number of Scheduled Job Executions

To execute a scheduled job a specific number of times only, you can configure it with the `numberOfExecutions` option. Its value is the number of times the scheduled job can be executed during the Medusa application's runtime.

For example:

```ts highlights={highlights}
export default async function myCustomJob() {
  console.log("I'll be executed only three times.")
}

export const config = {
  name: "hello-world",
  // execute every minute
  schedule: "* * * * *",
  numberOfExecutions: 3,
}
```

The above scheduled job has the `numberOfExecutions` configuration set to `3`.

So, Medusa will execute this job only 3 times, once every minute, and then it won't be executed anymore during the current runtime.

### Configuration is Per Application Runtime

Medusa tracks the number of executions for a scheduled job during its current runtime. Once the application stops, the next time you start it, the counter will be reset to `0`.

So, if you restart the Medusa application, the scheduled job will be executed again until it reaches the number of executions specified.


# Set Interval for Scheduled Jobs

In this chapter, you'll learn the difference between using intervals and crons for scheduled jobs, and how to use intervals.

## What is a Scheduled Job Interval?

A scheduled job interval is a specific duration that must pass between the execution of scheduled jobs. The timer starts when the Medusa application starts, and it resets every time a job is executed.

For example, if you have a scheduled job that runs on an interval of 5 minutes, the job will run after 5 minutes from application startup. Then, it will run again 5 minutes after the last execution, and so on.

## How to Set an Interval for a Scheduled Job?

To set an interval for a scheduled job, pass in the scheduled job configurations object the `schedule.interval` property with the desired duration.

For example:

```ts title="src/jobs/hello-world.ts" highlights={highlights}
import { MedusaContainer } from "@medusajs/framework/types"

export default async function greetingJob(container: MedusaContainer) {
  const logger = container.resolve("logger")

  logger.info("Greeting!")
}

export const config = {
  name: "greeting-every-minute",
  schedule: {
    interval: 60000, // 1 minute
  },
}
```

The `interval` property accepts a number indicating the duration in milliseconds.

So, the above scheduled job will run every minute, starting one minute after the Medusa application starts.

### Test the Scheduled Job Interval

To test out your scheduled job, start the Medusa application:

```bash npm2yarn
npm run dev
```

After a minute, the following message will be logged to the terminal:

```bash
info:    Greeting!
```

***

## Scheduled Job Interval vs. Scheduled Job Cron

In the [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) chapter, you learned about using the `schedule` configuration to set the schedule for a job using a cron expression. For example, `*/5 * * * *` would run a job every 5 minutes.

|Comparison Aspect|Cron Expression|Interval|
|---|---|---|
|Execution Timing|Specific times of the day. For example, at midnight.|After a specific duration between job executions. For example, every 5 minutes.|
|Execution Consistency|May not run if the specified time is reached and the system is busy|Will run after the specified interval, even if the system is busy|

While using a cron expression is ideal to ensure that the job runs at specific times of the day, the scheduled job will only run when the specified time is reached. So, if the events system is busy when the specified time is reached, the job will not run until the next scheduled time.

For example, if you have a job scheduled with a cron expression to run every 5 minutes, but the system is under heavy load, it may miss the 5-minute mark and not execute the job until the next scheduled time.

In contrast, a scheduled job interval is defined by a specific duration between job executions, regardless of the exact time they are scheduled to run. So, even if the system is busy, the job will still run after the specified interval has passed since the last execution.

### When to Use Intervals for Scheduled Jobs

If your scheduled job must run consistently but not at specific times of the day, use an **interval**. This ensures that the job runs after a defined duration, regardless of the system's state.

If your scheduled job must be executed at specific times of the day, use a cron **expression**. This allows for precise control over when the job is executed, but may result in missed executions if the system is under heavy load.


# Scheduled Jobs

In this chapter, you’ll learn about scheduled jobs and how to use them.

## What is a Scheduled Job?

When building your commerce application, you may need to automate tasks and run them repeatedly at a specific schedule. For example, you need to automatically sync products to a third-party service once a day.

In other commerce platforms, this feature isn't natively supported. Instead, you have to setup a separate application to execute cron jobs, which adds complexity as to how you expose this task to be executed in a cron job, or how you debug it when it's not running within the platform's tooling.

Medusa removes this overhead by supporting this feature natively with scheduled jobs. A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime. Your efforts are only spent on implementing the functionality performed by the job, such as syncing products to an ERP.

- You want the action to execute at a specified schedule while the Medusa application **isn't** running. Instead, create a [custom CLI script](https://docs.medusajs.com/learn/fundamentals/custom-cli-scripts/index.html.md) and execute it using the operating system's equivalent of a cron job.
- You want to execute the action once when the application loads. Use [loaders](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md) or [custom CLI scripts](https://docs.medusajs.com/learn/fundamentals/custom-cli-scripts#run-custom-script-on-application-startup/index.html.md) instead.
- You want to execute the action if an event occurs. Use [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) instead.

***

## How to Create a Scheduled Job?

You create a scheduled job in a TypeScript or JavaScript file under the `src/jobs` directory. The file exports the asynchronous function to run, and the configurations indicating the schedule to run the function.

For example, create the file `src/jobs/hello-world.ts` with the following content:

![Example of scheduled job file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732866423/Medusa%20Book/scheduled-job-dir-overview_ediqgm.jpg)

```ts title="src/jobs/hello-world.ts" highlights={highlights}
import { MedusaContainer } from "@medusajs/framework/types"

export default async function greetingJob(container: MedusaContainer) {
  const logger = container.resolve("logger")

  logger.info("Greeting!")
}

export const config = {
  name: "greeting-every-minute",
  schedule: "* * * * *",
}
```

You export an asynchronous function that receives the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) as a parameter. In the function, you resolve the [Logger utility](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) from the Medusa container and log a message.

You also export a `config` object that has the following properties:

- `name`: A unique name for the job.
- `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job.

This scheduled job executes every minute and logs into the terminal the message `Greeting!`.

### Test the Scheduled Job

To test out your scheduled job, start the Medusa application:

```bash npm2yarn
npm run dev
```

After a minute, the following message will be logged to the terminal:

```bash
info:    Greeting!
```

### Troubleshooting Scheduled Jobs

If your scheduled job is not running at the expected times, refer to the [Scheduled Job Troubleshooting Guide](https://docs.medusajs.com/resources/troubleshooting/scheduled-job-not-running/index.html.md).

***

## Example: Sync Products Once a Day

In a realistic scenario like syncing products to an ERP once a day, you should create a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) and execute it in a scheduled job.

A workflow is a task made up of a series of steps, and you construct it like you would a regular function, but it's a special function that supports rollback mechanism in case of errors, background execution, and more.

You can learn how to create a workflow in the [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) chapter, but this example assumes you already have a `syncProductToErpWorkflow` implemented. To execute this workflow once a day, create a scheduled job at `src/jobs/sync-products.ts` with the following content:

```ts title="src/jobs/sync-products.ts"
import { MedusaContainer } from "@medusajs/framework/types"
import { syncProductToErpWorkflow } from "../workflows/sync-products-to-erp"

export default async function syncProductsJob(container: MedusaContainer) {
  await syncProductToErpWorkflow(container)
    .run()
}

export const config = {
  name: "sync-products-job",
  schedule: "0 0 * * *",
}
```

In the scheduled job function, you execute the `syncProductToErpWorkflow` by invoking it and passing it the container, then invoking the `run` method. You also specify in the exported configurations the schedule `0 0 * * *` which indicates midnight time of every day.

The next time you start the Medusa application, it will run this job every day at midnight.


# Expose a Workflow Hook

In this chapter, you'll learn how to expose a hook in your workflow.

Refer to the [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) chapter to learn what a workflow hook is and how to consume Medusa's workflow hooks.

## When to Expose a Hook?

Medusa exposes hooks in many of its workflows to allow you to inject custom functionality.

You can also expose your own hooks in your workflows to allow other developers to consume them. This is useful when you're creating a workflow in a [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) and you want plugin users to extend the workflow's functionality.

For example, you are creating a blog plugin and you want to allow developers to perform custom validation before a blog post is created. You can expose a hook in your workflow that developers can consume to perform their custom validation.

If your workflow is not in a plugin, you probably don't need to expose a hook as you can perform the necessary actions directly in the workflow.

***

## How to Expose a Hook in a Workflow?

To expose a hook in your workflow, use `createHook` from the Workflows SDK.

For example:

```ts title="src/workflows/create-blog-post/index.ts" highlights={hookHighlights}
import {
  createStep,
  createHook,
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { createPostStep } from "./steps/create-post"

export const createBlogPostWorkflow = createWorkflow(
  "create-blog-post", 
  function (input) {
    const validate = createHook(
      "validate", 
      { post: input }
    )
    const post = createPostStep(input)

    return new WorkflowResponse(post, {
      hooks: [validate],
    })
  }
)
```

The `createHook` function accepts two parameters:

1. The first is a string indicating the hook's name. Developers consuming the hook will use this name to access the hook.
2. The second is the input to pass to the hook handler. Developers consuming the hook will receive this input in the hook handler.

You must also return the hook in the workflow's response by passing a `hooks` property to the `WorkflowResponse`'s second parameter object. Its value is an array of the workflow's hooks.

### How to Consume the Hook?

To consume the hook of the workflow, create the file `src/workflows/hooks/create-blog-post.ts` with the following content:

```ts title="src/workflows/hooks/create-blog-post.ts" highlights={handlerHighlights}
import { MedusaError } from "@medusajs/framework/utils"
import { createBlogPostWorkflow } from "../create-blog-post"

createBlogPostWorkflow.hooks.validate(
  async ({ post }, { container }) => {
    // TODO perform an action
    if (!post.additional_data.custom_title) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Custom title is required"
      )
    }
  }
)
```

The hook is available on the workflow's `hooks` property using its name `validate`.

You invoke the hook, passing a step function (the hook handler) as a parameter.

The hook handler is essentially a step function. You can perform in it any actions you perform in a step. For example, you can throw an error, which would stop the workflow execution.

You can also access the Medusa container in the hook handler to perform actions like using Query or module services.

For example:

```ts title="src/workflows/hooks/create-blog-post.ts"
import { createBlogPostWorkflow } from "../create-blog-post"

createBlogPostWorkflow.hooks.validate(
  async ({ post }, { container }) => {
    const query = container.resolve("query")

    const { data: existingPosts } = await query.graph({
      entity: "post",
      fields: ["*"],
    })

    // TODO do something with existing posts...
  }
)
```

Learn more about the hook handler in the [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) chapter.


# Compensation Function

In this chapter, you'll learn what a compensation function is and how to add it to a step.

## What is a Compensation Function

A compensation function rolls back or undoes changes made by a step when an error occurs in the workflow.

For example, if a step creates a record, the compensation function deletes the record when an error occurs later in the workflow.

By using compensation functions, you provide a mechanism that guarantees data consistency in your application and across systems.

***

## How to add a Compensation Function?

A compensation function is passed as a second parameter to the `createStep` function.

For example, create the file `src/workflows/hello-world.ts` with the following content:

```ts title="src/workflows/hello-world.ts" highlights={[["15"], ["16"], ["17"]]} collapsibleLines="1-5" expandButtonLabel="Show Imports"
import { 
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async () => {
    const message = `Hello from step one!`

    console.log(message)

    return new StepResponse(message)
  },
  async () => {
    console.log("Oops! Rolling back my changes...")
  }
)
```

Each step can have a compensation function. The compensation function only runs if an error occurs throughout the workflow.

***

## Test the Compensation Function

Create a step in the same `src/workflows/hello-world.ts` file that throws an error:

```ts title="src/workflows/hello-world.ts"
const step2 = createStep(
  "step-2",
  async () => {
    throw new Error("Throwing an error...")
  }
)
```

Then, create a workflow that uses the steps:

```ts title="src/workflows/hello-world.ts" collapsibleLines="1-8" expandButtonLabel="Show Imports"
import {
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
// other imports...

// steps...

const myWorkflow = createWorkflow(
  "hello-world", 
  function (input) {
  const str1 = step1()
  step2()

  return new WorkflowResponse({
    message: str1,
  })
})

export default myWorkflow
```

Finally, execute the workflow from an API route:

```ts title="src/api/workflow/route.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import myWorkflow from "../../../workflows/hello-world"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await myWorkflow(req.scope)
    .run()

  res.send(result)
}
```

Run the Medusa application and send a `GET` request to `/workflow`:

```bash
curl http://localhost:9000/workflow
```

In the console, you'll see:

- `Hello from step one!` logged in the terminal, indicating that the first step ran successfully.
- `Oops! Rolling back my changes...` logged in the terminal, indicating that the second step failed and the compensation function of the first step ran consequently.

***

## Pass Input to Compensation Function

If a step creates a record, the compensation function must receive the ID of the record to remove it.

To pass input to the compensation function, pass a second parameter in the `StepResponse` returned by the step.

For example:

```ts highlights={inputHighlights}
import { 
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async () => {
    return new StepResponse(
      `Hello from step one!`, 
      { message: "Oops! Rolling back my changes..." }
    )
  },
  async ({ message }) => {
    console.log(message)
  }
)
```

In this example, the step passes an object as a second parameter to `StepResponse`.

The compensation function receives the object and uses its `message` property to log a message.

***

## Resolve Resources from the Medusa Container

The compensation function receives an object second parameter. The object has a `container` property that you use to resolve resources from the Medusa container.

For example:

```ts
import { 
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

const step1 = createStep(
  "step-1",
  async () => {
    return new StepResponse(
      `Hello from step one!`, 
      { message: "Oops! Rolling back my changes..." }
    )
  },
  async ({ message }, { container }) => {
    const logger = container.resolve(
      ContainerRegistrationKeys.LOGGER
    )

    logger.info(message)
  }
)
```

In this example, you use the `container` property in the second object parameter of the compensation function to resolve the logger.

You then use the logger to log a message.

***

## Handle Step Errors in Loops

This feature is only available after [Medusa v2.0.5](https://github.com/medusajs/medusa/releases/tag/v2.0.5).

Consider you have a module that integrates a third-party ERP system, and you're creating a workflow that deletes items in that ERP. You may have the following step:

```ts
// other imports...
import { promiseAll } from "@medusajs/framework/utils"

type StepInput = {
  ids: string[]
}

const step1 = createStep(
  "step-1",
  async ({ ids }: StepInput, { container }) => {
    const erpModuleService = container.resolve(
      ERP_MODULE
    )
    const prevData: unknown[] = []

    await promiseAll(
      ids.map(async (id) => {
        const data = await erpModuleService.retrieve(id)

        await erpModuleService.delete(id)

        prevData.push(id)
      })
    )

    return new StepResponse(ids, prevData)
  }
)
```

In the step, you loop over the IDs to retrieve the item's data, store them in a `prevData` variable, then delete them using the ERP Module's service. You then pass the `prevData` variable to the compensation function.

However, if an error occurs in the loop, the `prevData` variable won't be passed to the compensation function as the execution never reached the return statement.

To handle errors in the loop so that the compensation function receives the last version of `prevData` before the error occurred, you wrap the loop in a try-catch block. Then, in the catch block, you invoke and return the `StepResponse.permanentFailure` function:

```ts highlights={highlights}
try {
  await promiseAll(
    ids.map(async (id) => {
      const data = await erpModuleService.retrieve(id)

      await erpModuleService.delete(id)

      prevData.push(id)
    })
  )
} catch (e) {
  return StepResponse.permanentFailure(
    `An error occurred: ${e}`,
    prevData
  )
}
```

The `StepResponse.permanentFailure` fails the step and its workflow, triggering current and previous steps' compensation functions. The `permanentFailure` function accepts as a first parameter the error message, which is saved in the workflow's error details, and as a second parameter the data to pass to the compensation function.

So, if an error occurs during the loop, the compensation function will still receive the `prevData` variable to undo the changes made before the step failed.

For more details on error handling in workflows and steps, check the [Handling Errors chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md).


# Conditions in Workflows with When-Then

In this chapter, you'll learn how to execute an action based on a condition in a workflow using when-then from the Workflows SDK.

## Why If-Conditions Aren't Allowed in Workflows?

Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store the workflow's steps. At that point, variables in the workflow don't have any values. They only do when you execute the workflow.

So, you can't use an if-condition that checks a variable's value, as the condition will be evaluated when Medusa creates the internal representation of the workflow, rather than during execution.

Instead, use `when-then` from the Workflows SDK. It allows you to perform steps in a workflow only if a condition that you specify is satisfied.

Restrictions for conditions is only applicable in a workflow's definition. You can still use if-conditions in your step's code.

***

## How to use When-Then?

The Workflows SDK provides a `when` function that is used to check whether a condition is true. You chain a `then` function to `when` that specifies the steps to execute if the condition in `when` is satisfied.

For example:

```ts highlights={highlights}
import { 
  createWorkflow,
  WorkflowResponse,
  when,
} from "@medusajs/framework/workflows-sdk"
// step imports...

const workflow = createWorkflow(
  "workflow", 
  function (input: {
    is_active: boolean
  }) {

    const result = when(
      input, 
      (input) => {
        return input.is_active
      }
    ).then(() => {
      const stepResult = isActiveStep()
      return stepResult
    })

    // executed without condition
    const anotherStepResult = anotherStep(result)

    return new WorkflowResponse(
      anotherStepResult
    )
  }
)
```

In this code snippet, you execute the `isActiveStep` only if the `input.is_active`'s value is `true`.

### When Parameters

`when` accepts two parameters:

1. The first parameter is either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter.
2. The second parameter is a function that returns a boolean indicating whether to execute the action in `then`.
   - You can perform conditional checks or use conditional operators in this function, but only on the function's parameters.

### Then Callback Function

To specify the action to perform if the condition is satisfied, chain a `then` function to `when` and pass it a callback function. The callback function is only executed if `when`'s second parameter function returns a `true` value.

The callback function passed to `then` is treated the same as the workflow's definition function. That means:

- You can execute steps in the callback function.
- You can't [transform data and variables](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) in the callback function. Instead, use `transform`.
- You can't use if-else conditions in the callback function.
- You can't use loops in the callback function.
- You should keep in mind the [workflow constructor constraints](https://docs.medusajs.com/learn/fundamentals/workflows/constructor-constraints/index.html.md) in the callback function.

***

## Implementing If-Else with When-Then

`when-then` doesn't support if-else conditions. Instead, use two `when-then` conditions in your workflow.

For example:

```ts highlights={ifElseHighlights}
const workflow = createWorkflow(
  "workflow", 
  function (input: {
    is_active: boolean
  }) {

    const isActiveResult = when(
      input, 
      (input) => {
        return input.is_active
      }
    ).then(() => {
      return isActiveStep()
    })

    const notIsActiveResult = when(
      input,
      (input) => {
        return !input.is_active
      }
    ).then(() => {
      return notIsActiveStep()
    })

    // ...
  }
)
```

In the above workflow, you use two `when-then` blocks. The first one performs a step if `input.is_active` is `true`, and the second performs a step if `input.is_active` is `false`, acting as an else condition.

***

## Specify Name for When-Then

Internally, `when-then` blocks have a unique name similar to a step. When you return a step's result in a `when-then` block, the block's name is derived from the step's name. For example:

```ts
const isActiveResult = when(
  input, 
  (input) => {
    return input.is_active
  }
).then(() => {
  return isActiveStep()
})
```

This `when-then` block's internal name will be `when-then-is-active`, where `is-active` is the step's name.

However, if you need to return in your `when-then` block something other than a step's result, you need to specify a unique step name for that block. Otherwise, Medusa will generate a random name for it which can cause unexpected errors in production.

You pass a name for `when-then` as a first parameter of `when`, whose signature can accept three parameters in this case. For example:

```ts highlights={nameHighlights}
const { isActive } = when(
  "check-is-active",
  input, 
  (input) => {
    return input.is_active
  }
).then(() => {
  const isActive = isActiveStep()

  const returnData = transform({
    isActive,
  }, (data) => {
    return {
      message: data.isActive ? "Active" : "Not Active",
    }
  })

  return returnData
})
```

Since `then` returns a value different than the step's result, you pass to the `when` function the following parameters:

1. A unique name to be assigned to the `when-then` block.
2. Either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter.
3. A function that returns a boolean indicating whether to execute the action in `then`.

The second and third parameters are the same as the parameters you previously passed to `when`.


# Workflow Constraints

This chapter lists constraints of defining a workflow or its steps.

Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. At that point, variables in the workflow don't have any values. They only do when you execute the workflow.

This creates restrictions related to variable manipulations, using if-conditions, and other constraints. This chapter lists these constraints and provides their alternatives.

## Workflow Constraints

### No Async Functions

The function passed to `createWorkflow` can’t be an async function:

```ts highlights={[["4", "async", "Function can't be async."], ["11", "", "Correct way of defining the function."]]}
// Don't
const myWorkflow = createWorkflow(
  "hello-world", 
  async function (input: WorkflowInput) {
  // ...
})

// Do
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
  // ...
})
```

### No Direct Variable Manipulation

You can’t directly manipulate variables within the workflow's constructor function.

Learn more about why you can't manipulate variables [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md)

Instead, use `transform` from the Workflows SDK:

```ts highlights={highlights}
// Don't
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
  const str1 = step1(input)
  const str2 = step2(input)

  return new WorkflowResponse({
    message: `${str1}${str2}`,
  })
})

// Do
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
  const str1 = step1(input)
  const str2 = step2(input)

  const result = transform(
    {
      str1,
      str2,
    },
    (input) => ({
      message: `${input.str1}${input.str2}`,
    })
  )

  return new WorkflowResponse(result)
})
```

#### Create Dates in transform

When you use `new Date()` in a workflow's constructor function, the date is evaluated when Medusa creates the internal representation of the workflow, not during execution.

Instead, create the date using `transform`.

Learn more about how Medusa creates an internal representation of a workflow [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md).

For example:

```ts highlights={dateHighlights}
// Don't
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
  const today = new Date()

  return new WorkflowResponse({
    today,
  })
})

// Do
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
  const today = transform({}, () => new Date())

  return new WorkflowResponse({
    today,
  })
})
```

### No If Conditions

You can't use if-conditions in a workflow.

Learn more about why you can't use if-conditions [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows/index.html.md)

Instead, use when-then from the Workflows SDK:

```ts
// Don't
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
  if (input.is_active) {
    // perform an action
  }
})

// Do (explained in the next chapter)
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
  when(input, (input) => {
    return input.is_active
  })
  .then(() => {
    // perform an action
  })
})
```

You can also pair multiple `when-then` blocks to implement an `if-else` condition as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md).

### No Conditional Operators

You can't use conditional operators in a workflow, such as `??` or `||`.

Learn more about why you can't use conditional operators [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows/index.html.md)

Instead, use `transform` to store the desired value in a variable.

#### Logical Or (||) Alternative

```ts
// Don't
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
    const message = input.message || "Hello"
})

// Do
// other imports...
import { transform } from "@medusajs/framework/workflows-sdk"

const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
    const message = transform(
      {
        input,
      },
      (data) => data.input.message || "hello"
    )
})
```

#### Nullish Coalescing (??) Alternative

```ts
// Don't
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
    const message = input.message ?? "Hello"
})

// Do
// other imports...
import { transform } from "@medusajs/framework/workflows-sdk"

const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
    const message = transform(
      {
        input,
      },
      (data) => data.input.message ?? "hello"
    )
})
```

#### Double Not (!!) Alternative

```ts
// Don't
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
    step1({
      isActive: !!input.is_active,
    })
})

// Do
// other imports...
import { transform } from "@medusajs/framework/workflows-sdk"

const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
    const isActive = transform(
      {
        input,
      },
      (data) => !!data.input.is_active
    )
    
    step1({
      isActive,
    })
})
```

#### Ternary Alternative

```ts
// Don't
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
    step1({
      message: input.is_active ? "active" : "inactive",
    })
})

// Do
// other imports...
import { transform } from "@medusajs/framework/workflows-sdk"

const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
    const message = transform(
      {
        input,
      },
      (data) => {
        return data.input.is_active ? "active" : "inactive"
      }
    )
    
    step1({
      message,
    })
})
```

#### Optional Chaining (?.) Alternative

```ts
// Don't
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
    step1({
      name: input.customer?.name,
    })
})

// Do
// other imports...
import { transform } from "@medusajs/framework/workflows-sdk"

const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
    const name = transform(
      {
        input,
      },
      (data) => data.input.customer?.name
    )
    
    step1({
      name,
    })
})
```

### No Try-Catch

In a workflow, don't use try-catch blocks to handle errors.

Instead, refer to the [Error Handling](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md) chapter for alternative ways to handle errors.

***

## Returned Value Constraints

Data returned from workflows and steps are serialized, allowing Medusa to store them in the database. So, you must only return serializable values, such as [primitive values](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#primitive_values) or an object, from workflows and steps.

Values of other types, such as Maps, aren't allowed.

```ts
// Don't
import { 
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  (input, { container }) => {
    const myMap = new Map()

    // ...

    return new StepResponse({
      myMap,
    })
  }
)

// Do
import { 
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  (input, { container }) => {
    const myObj: Record<string, unknown> = {}

    // ...

    return new StepResponse({
      myObj,
    })
  }
)
```

### Buffer Example

In some cases, you may need to return a buffer. For example, when your workflow generates a file and you want to return it as a buffer.

In those cases, you can return an object containing the buffer as a property. Then, in customizations that execute the workflow, you can recreate the buffer from the serialized data.

For example, consider the following workflow that returns a buffer:

```ts
import { 
  createWorkflow,
  createStep,
  WorkflowResponse,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  (_, { container }) => {
    const buffer = Buffer.from("Hello, World!")

    return new StepResponse({
      buffer,
    })
  }
)

const myWorkflow = createWorkflow(
  "hello-world", 
  function () {
    const step1Response = step1()

    return new WorkflowResponse({
      buffer: step1Response.buffer,
    })
  }
)
```

Then, in an API route that executes this workflow, you can recreate the buffer from the serialized data using `Buffer.from`:

```ts
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import myWorkflow from "../../workflows/hello-world"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await myWorkflow(req.scope)
    .run()

  const buffer = Buffer.from(result.buffer)

  res.setHeader("Content-Type", "application/octet-stream")
  res.setHeader("Content-Disposition", "attachment; filename=hello.txt")
  res.send(buffer)
}
```


# Error Handling in Workflows

In this chapter, you’ll learn about what happens when an error occurs in a workflow, how to disable error throwing in a workflow, and try-catch alternatives in workflow definitions.

## Default Behavior of Errors in Workflows

When an error occurs in a workflow, such as when a step throws an error, the workflow execution stops. Then, [the compensation function](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md) of every step in the workflow is called to undo the actions performed by their respective steps.

The workflow's caller, such as an API route, subscriber, or scheduled job, will also fail and stop execution. Medusa then logs the error in the console. For API routes, an appropriate error is returned to the client based on the thrown error.

Learn more about error handling in API routes in the [Errors chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/errors/index.html.md).

This is the default behavior of errors in workflows. However, you can configure workflows to not throw errors, or you can configure a step's internal error handling mechanism to change the default behavior.

***

## Disable Error Throwing in Workflow

When an error is thrown in the workflow, that means the caller of the workflow, such as an API route, will fail and stop execution as well.

While this is the common behavior, there are certain cases where you want to handle the error differently. For example, you may want to check the errors thrown by the workflow and return a custom error response to the client.

The object parameter of a workflow's `run` method accepts a `throwOnError` property. When this property is set to `false`, the workflow will stop execution if an error occurs, but the Medusa's workflow engine will catch that error and return it to the caller instead of throwing it.

For example:

```ts title="src/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import myWorkflow from "../../../workflows/hello-world"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result, errors } = await myWorkflow(req.scope)
    .run({
      // ...
      throwOnError: false,
    })

  if (errors.length) {
    return res.send({
      message: "Something unexpected happened. Please try again.",
    })
  }

  res.send(result)
}
```

You disable throwing errors in the workflow by setting the `throwOnError` property to `false` in the `run` method of the workflow.

The object returned by the `run` method contains an `errors` property. This property is an array of errors that occured during the workflow's execution. You can check this array to see if any errors occurred and handle them accordingly.

An error object has the following properties:

- action: (\`string\`) The ID of the step that threw the error.
- handlerType: (\`invoke\` \\| \`compensate\`) Where the error occurred. If the value is \`invoke\`, it means the error occurred in a step. Otherwise, the error occurred in the compensation function of a step.
- error: (\[Error]\(https://nodejs.org/docs/latest-v20.x/api/errors.html#class-error)) The error object that was thrown.

***

## Try-Catch Alternatives in Workflow Definition

If you want to use try-catch mechanism in a workflow to undo step actions, use a [compensation function](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md) instead.

### Why You Can't Use Try-Catch in Workflow Definitions

Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps.

At that point, variables in the workflow don't have any values. They only do when you execute the workflow.

So, try-catch blocks in the workflow definition function won't have an effect, as at that time the workflow is not executed and errors are not thrown.

You can still use try-catch blocks in a workflow's step functions. For cases that require granular control over error handling in a workflow's definition, you can configure the internal error handling mechanism of a step.

### Skip Workflow on Step Failure

A step has a `skipOnPermanentFailure` configuration that allows you to configure what happens when an error occurs in the step. Its value can be a boolean or a string.

By default, `skipOnPermanentFailure` is disabled. When it's enabled, the workflow's status is set to `skipped` instead of `failed`. This means:

- Compensation functions of the workflow's steps are not called.
- The workflow's caller continues executing. You can still [access the error](#disable-error-throwing-in-workflow) that occurred during the workflow's execution as mentioned in the [Disable Error Throwing](#disable-error-throwing-in-workflow) section.

This is useful when you want to perform actions if no error occurs, but you don't care about compensating the workflow's steps or you don't want to stop the caller's execution.

You can think of setting the `skipOnPermanentFailure` to `true` as the equivalent of the following `try-catch` block:

```ts title="Outside a Workflow"
try {
  actionThatThrowsError()

  moreActions()
} catch (e) {
  // don't do anything
}
```

You can do this in a workflow using the step's `skipOnPermanentFailure` configuration:

```ts title="Workflow Equivalent" highlights={skipOnPermanentFailureEnabledHighlights}
import {
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import { 
  actionThatThrowsError,
  moreActions,
} from "./steps"

export const myWorkflow = createWorkflow(
  "hello-world", 
  function (input) {
    actionThatThrowsError().config({
      skipOnPermanentFailure: true,
    })

    // This action will not be executed if the previous step throws an error
    moreActions()
  }
)
```

You set the configuration of a step by chaining the `config` method to the step's function call. The `config` method accepts an object similar to the one that can be passed to `createStep`.

In this example, if the `actionThatThrowsError` step throws an error, the rest of the workflow will be skipped, and the `moreActions` step will not be executed.

You can then access the error that occurred in that step as explained in the [Disable Error Throwing](#disable-error-throwing-in-workflow) section.

### Continue Workflow Execution from a Specific Step

In some cases, if an error occurs in a step, you may want to continue the workflow's execution from a specific step instead of stopping the workflow's execution or skipping the rest of the steps.

The `skipOnPermanentFailure` configuration can accept a step's ID as a value. Then, the workflow will continue execution from that step if an error occurs in the step that has the `skipOnPermanentFailure` configuration.

The compensation function of the step that has the `skipOnPermanentFailure` configuration will not be called when an error occurs.

You can think of setting the `skipOnPermanentFailure` to a step's ID as the equivalent of the following `try-catch` block:

```ts title="Outside a Workflow"
try {
  actionThatThrowsError()

  moreActions()
} catch (e) {
  // do nothing
}

continueExecutionFromStep()
```

You can do this in a workflow using the step's `skipOnPermanentFailure` configuration:

```ts title="Workflow Equivalent" highlights={skipOnPermanentFailureStepHighlights}
import {
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import { 
  actionThatThrowsError,
  moreActions,
  continueExecutionFromStep,
} from "./steps"

export const myWorkflow = createWorkflow(
  "hello-world", 
  function (input) {
    actionThatThrowsError().config({
      // The `continue-execution-from-step` is the ID passed as a first
      // parameter to `createStep` of `continueExecutionFromStep`.
      skipOnPermanentFailure: "continue-execution-from-step",
    })

    // This action will not be executed if the previous step throws an error
    moreActions()

    // This action will be executed either way
    continueExecutionFromStep()
  }
)
```

In this example, you configure the `actionThatThrowsError` step to continue the workflow's execution from the `continueExecutionFromStep` step if an error occurs in the `actionThatThrowsError` step.

Notice that you pass the ID of the `continueExecutionFromStep` step as it's set in the `createStep` function.

So, the `moreActions` step will not be executed if the `actionThatThrowsError` step throws an error, and the `continueExecutionFromStep` will be executed anyway.

You can then access the error that occurred in that step as explained in the [Disable Error Throwing](#disable-error-throwing-in-workflow) section.

If the specified step ID doesn't exist in the workflow, it will be equivalent to setting the `skipOnPermanentFailure` configuration to `true`. So, the workflow will be skipped, and the rest of the steps will not be executed.

### Set Step as Failed, but Continue Workflow Execution

In some cases, you may want to fail a step, but continue the rest of the workflow's execution.

This is useful when you don't want a step's failure to stop the workflow's execution, but you want to mark that step as failed.

The `continueOnPermanentFailure` configuration allows you to do that. When enabled, the workflow's execution will continue, but the step will be marked as failed if an error occurs in that step.

The compensation function of the step that has the `continueOnPermanentFailure` configuration will not be called when an error occurs.

You can think of setting the `continueOnPermanentFailure` to `true` as the equivalent of the following `try-catch` block:

```ts title="Outside a Workflow"
try {
  actionThatThrowsError()
} catch (e) {
  // do nothing
}

moreActions()
```

You can do this in a workflow using the step's `continueOnPermanentFailure` configuration:

```ts title="Workflow Equivalent" highlights={continueOnPermanentFailureHighlights}
import {
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import { 
  actionThatThrowsError,
  moreActions,
} from "./steps"

export const myWorkflow = createWorkflow(
  "hello-world", 
  function (input) {
    actionThatThrowsError().config({
      continueOnPermanentFailure: true,
    })

    // This action will be executed even if the previous step throws an error
    moreActions()
  }
)
```

In this example, if the `actionThatThrowsError` step throws an error, the `moreActions` step will still be executed.

You can then access the error that occurred in that step as explained in the [Disable Error Throwing](#disable-error-throwing-in-workflow) section.


# Execute Nested Workflows

In this chapter, you'll learn how to execute a workflow in another workflow.

## How to Execute a Workflow in Another?

In many cases, you may have a workflow that you want to re-use in another workflow. This is most common when you build custom workflows and you want to utilize Medusa's [existing workflows](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md).

Executing a workflow within another is slightly different from how you usually execute a workflow. Instead of invoking the workflow, passing it the container, then running its `run` method, you use the `runAsStep` method of the workflow. This will pass the Medusa container and workflow context to the nested workflow.

For example, to execute the [createProductsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md) in your custom workflow:

```ts highlights={workflowsHighlights} collapsibleLines="1-7" expandMoreButton="Show Imports"
import {
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import { 
  createProductsWorkflow,
} from "@medusajs/medusa/core-flows"

const workflow = createWorkflow(
  "hello-world",
  async (input) => {
    const products = createProductsWorkflow.runAsStep({
      input: {
        products: [
          // ...
        ],
      },
    })

    // ...
  }
)
```

The `runAsStep` method accepts an `input` property to pass input to the workflow.

### Returned Data

Notice that you don't need to use `await` when executing the nested workflow, as it's not a promise in this scenario.

You also receive the workflow's output as a return value from the `runAsStep` method. This is different from the usual workflow response, where you receive the output in a `result` property.

***

## Prepare Input Data

Since Medusa creates an internal representation of your workflow's constructor function, you can't manipulate data directly in the workflow constructor. You can learn more about this in the [Data Manipulation](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) chapter.

If you need to perform some data manipulation to prepare the nested workflow's input data, use `transform` from the Workflows SDK.

For example:

```ts highlights={transformHighlights} collapsibleLines="1-12"
import {
  createWorkflow,
  transform,
} from "@medusajs/framework/workflows-sdk"
import { 
  createProductsWorkflow,
} from "@medusajs/medusa/core-flows"

type WorkflowInput = {
  title: string
}

const workflow = createWorkflow(
  "hello-product",
  async (input: WorkflowInput) => {
    const createProductsData = transform({
      input,
    }, (data) => [
      {
        title: `Hello ${data.input.title}`,
      },
    ])

    const products = createProductsWorkflow.runAsStep({
      input: {
        products: createProductsData,
      },
    })

    // ...
  }
)
```

In this example, you use the `transform` function to prepend `Hello` to the title of the product. Then, you pass the result as input to the `createProductsWorkflow`.

Learn more about `transform` in the [Data Manipulation](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) chapter.

***

## Run Workflow Conditionally

Similar to the [previous section](#prepare-input-data), you can't use conditional statements directly in the workflow constructor. Instead, you can use the `when-then` function from the Workflows SDK to run a workflow conditionally.

For example:

```ts highlights={whenHighlights} collapsibleLines="1-16"
import {
  createWorkflow,
  when,
} from "@medusajs/framework/workflows-sdk"
import { 
  createProductsWorkflow,
} from "@medusajs/medusa/core-flows"
import { 
  CreateProductWorkflowInputDTO,
} from "@medusajs/framework/types"

type WorkflowInput = {
  product?: CreateProductWorkflowInputDTO
  should_create?: boolean
}

const workflow = createWorkflow(
  "hello-product",
  async (input: WorkflowInput) => {
    const product = when(input, ({ should_create }) => should_create)
      .then(() => {
        return createProductsWorkflow.runAsStep({
          input: {
            products: [input.product],
          },
        })
      })
  }
)
```

In this example, you use `when-then` to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled.

Learn more about `when-then` in the [When-Then Conditions](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md) chapter.

***

## Errors in Nested Workflows

A nested workflow behaves similarly to a step in a workflow. So, if the nested workflow fails, it will throw an error that stops the parent workflow's execution and compensates previous steps.

In addition, if another step fails after the nested workflow, the nested workflow's steps will be compensated as part of the compensation process.

Learn more about handling errors in workflows in the [Error Handling](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md) chapter.

***

## Nested Long-Running Workflows

When you execute a long-running workflow within another workflow, the parent workflow becomes a long-running workflow as well.

So, the parent workflow will wait for the nested workflow to finish before continuing its execution.

Refer to the [Long-Running Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md) chapter for more information on how to handle long-running workflows.


# Locking Operations in Workflows

In this chapter, you’ll learn how to lock operations in your workflows to avoid race conditions and ensure data consistency.

## What is a Lock?

A lock is a mechanism that restricts multiple accesses to a resource or a piece of code, preventing multiple processes from modifying it simultaneously.

Locks are particularly useful in workflows where concurrent executions may lead to commerce risks. For example, Medusa uses locks in its cart workflows to prevent issues like overselling products or double charging customers.

The [Locking Module](https://docs.medusajs.com/resources/infrastructure-modules/locking/index.html.md) handles the locking mechanism using the underlying provider. You can use the Locking Module's service in your workflows to create locks around critical sections of your code.

***

## How to Use Locks in Workflows

Medusa provides two steps that you can use to create locks in your workflows:

- [acquireLockStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/acquireLockStep/index.html.md): Attempt to acquire a lock. If the lock is already held by another process, this step will wait until the lock is released. It will fail if a timeout occurs before acquiring the lock.
- [releaseLockStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/releaseLockStep/index.html.md): Release a previously acquired lock.

You can use these steps in your workflows to ensure that only one instance of a workflow can modify a resource at a time.

For example:

```ts title="src/workflows/charge-customer.ts" highlights={workflowLockHighlights}
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
import { acquireLockStep, releaseLockStep } from "@medusajs/medusa/core-flows"
import { chargeCustomerStep } from "./steps/charge-customer-step"

type WorkflowInput = {
  customer_id: string;
  order_id: string;
}

export const chargeCustomerWorkflow = createWorkflow(
  "charge-customer",
  (input: WorkflowInput) => {
    acquireLockStep({
      key: input.order_id,
      // Attempt to acquire the lock for two seconds before timing out
      timeout: 2,
      // Lock is only held for a maximum of ten seconds
      ttl: 10,
    })

    chargeCustomerStep(input)

    releaseLockStep({
      key: input.order_id,
    })
  }
)
```

In this example, the workflow attempts to acquire a lock on an order using its ID as the lock key.

Once the lock is acquired, it executes the `chargeCustomerStep`. After the step is complete, it releases the lock using the `releaseLockStep`.

If the lock cannot be acquired within the specified timeout period, the `acquireLockStep` will throw an error, and the workflow will fail.

If an error occurs in the workflow after a lock is acquired with `acquireLockStep`, the step's compensation function will release the lock automatically.

### Locking Steps API

Refer to the [acquireLockStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/acquireLockStep/index.html.md) and [releaseLockStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/releaseLockStep/index.html.md) for more information on the inputs of these steps.

***

## How to Use Locks in Workflow Steps

You can alternatively acquire and release locks within steps by resolving the Locking Module's service and using its `acquire` and `release` methods.

Do not acquire locks within a workflow and a step in the same workflow execution, as this can lead to deadlocks.

For example:

```ts title="src/workflows/steps/charge-customer.ts"
import { createStep } from "@medusajs/framework/workflows-sdk"

type StepInput = {
  order_id: string
  customer_id: string
}

export const chargeCustomerStep = createStep(
  "charge-customer",
  async (input: StepInput, { container }) => {
    const lockingModuleService = container.resolve("locking")

    await lockingModuleService.acquire(input.order_id, {
      // Lock will auto-expire after 10 seconds
      expire: 10,
    })

    // TODO: charge customer

    await lockingModuleService.release(input.order_id)
  }
)
```

In this example, the `chargeCustomerStep` step acquires a lock on the order using the `order_id` as the lock key. After performing the operation, it releases the lock.

The step will wait until the lock is acquired based on the configured Locking Module Provider's options. For example, the [Redis Locking Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/locking/redis/index.html.md) will wait five seconds before timing out.

### Locking Module Service API

Refer to the [Locking Module reference](https://docs.medusajs.com/resources/references/locking-service/index.html.md) for more information on the Locking Module's service methods.

***

## Locks in Nested Workflows

When a [Nested workflows](https://docs.medusajs.com/learn/fundamentals/workflows/execute-another-workflow/index.html.md) acquires a lock with `acquireLockStep`, the lock is not acquired. So, if you're using a workflow that acquires locks with `acquireLockStep` within another workflow, the parent workflow must handle the locking mechanism.

If the nested workflow acquires a lock in a step using the Locking Module's service, the lock will be acquired as expected.

For example, consider a custom workflow that executes Medusa's [completeCartWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/completeCartWorkflow/index.html.md). The `completeCartWorkflow` acquires locks on the cart with `acquireLockStep` to prevent race conditions.

Therefore, the custom workflow must acquire and release the locks around the `completeCartWorkflow` execution. For example:

```ts title="src/workflows/custom-complete-cart.ts" highlights={nestedWorkflowLockHighlights}
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
import { 
  acquireLockStep, 
  releaseLockStep,
  completeCartWorkflow,
} from "@medusajs/medusa/core-flows"

type WorkflowInput = {
  cart_id: string;
}

export const customCompleteCartWorkflow = createWorkflow(
  "custom-complete-cart",
  (input: WorkflowInput) => {
    // acquire the same lock as the nested workflow
    acquireLockStep({
      key: input.cart_id,
      timeout: 30,
      ttl: 60 * 2,
    })

    // Execute the nested workflow
    completeCartWorkflow.runAsStep({
      input: {
        id: input.cart_id,
      },
    })

    // release the lock after the nested workflow is complete
    releaseLockStep({
      key: input.cart_id,
    })
  }
)
```

In the example above, the `customCompleteCartWorkflow` acquires a lock on the cart before executing the `completeCartWorkflow` and releases it afterward.

Make sure you're acquiring the same lock key as the nested workflow.


# Long-Running Workflows

In this chapter, you’ll learn what a long-running workflow is and how to configure it.

## What is a Long-Running Workflow?

When you execute a workflow, you wait until the workflow finishes execution to receive the output.

A long-running workflow is a workflow that continues its execution in the background. You don’t receive its output immediately. Instead, you subscribe to the workflow execution to listen to status changes and receive its result once the execution is finished.

### Why use Long-Running Workflows?

Long-running workflows are useful if:

- A task takes too long. For example, you're importing data from a CSV file.
- The workflow's steps wait for an external action to finish before resuming execution. For example, before you import the data from the CSV file, you wait until the import is confirmed by the user.
- You want to retry workflow steps after a long period of time. For example, you want to retry a step that processes a payment every day until the payment is successful.
  - Refer to the [Retry Failed Steps chapter](https://docs.medusajs.com/learn/fundamentals/workflows/retry-failed-steps/index.html.md) for more information.

***

## Configure Long-Running Workflows

A workflow is considered long-running if at least one step has its `async` configuration set to `true` and doesn't return a step response.

For example, consider the following workflow and steps:

```ts title="src/workflows/hello-world.ts" highlights={[["15"]]} collapsibleLines="1-11" expandButtonLabel="Show More"
import { 
  createStep,  
  createWorkflow,
  WorkflowResponse,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep("step-1", async () => {
  return new StepResponse({})
})

const step2 = createStep(
  {
    name: "step-2",
    async: true,
  },
  async () => {
    console.log("Waiting to be successful...")
  }
)

const step3 = createStep("step-3", async () => {
  return new StepResponse("Finished three steps")
})

const myWorkflow = createWorkflow(
  "hello-world", 
  function () {
  step1()
  step2()
  const message = step3()

  return new WorkflowResponse({
    message,
  })
})

export default myWorkflow
```

The second step has in its configuration object `async` set to `true` and it doesn't return a step response. This indicates that this step is an asynchronous step.

So, when you execute the `hello-world` workflow, it continues its execution in the background once it reaches the second step.

### When is a Workflow Considered Long-Running?

A workflow is also considered long-running if:

- One of its steps has its `async` configuration set to `true` and doesn't return a step response.
- One of its steps has its `retryInterval` option set as explained in the [Retry Failed Steps chapter](https://docs.medusajs.com/learn/fundamentals/workflows/retry-failed-steps/index.html.md).
- One of its [nested workflows](https://docs.medusajs.com/learn/fundamentals/workflows/execute-another-workflow/index.html.md) is a long-running workflow.

***

## Change Step Status

Once the workflow's execution reaches an async step, it'll wait in the background for the step to succeed or fail before it moves to the next step.

To fail or succeed a step, use the Workflow Engine Module's main service that is registered in the Medusa Container under the `Modules.WORKFLOW_ENGINE` (or `workflowsModuleService`) key.

### Retrieve Transaction ID

Before changing the status of a workflow execution's async step, you must have the execution's transaction ID.

When you execute the workflow, the object returned has a `transaction` property, which is an object that holds the details of the workflow execution's transaction. Use its `transactionId` to later change async steps' statuses:

```ts
const { transaction } = await myWorkflow(req.scope)
  .run()

// use transaction.transactionId later
```

### Change Step Status to Successful

The Workflow Engine Module's main service has a `setStepSuccess` method to set a step's status to successful. If you use it on a workflow execution's async step, the workflow continues execution to the next step.

For example, consider the following step:

```ts highlights={successStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
import {
  Modules,
  TransactionHandlerType,
} from "@medusajs/framework/utils"
import { 
  StepResponse, 
  createStep,
} from "@medusajs/framework/workflows-sdk"

type SetStepSuccessStepInput = {
  transactionId: string
};

export const setStepSuccessStep = createStep(
  "set-step-success-step",
  async function (
    { transactionId }: SetStepSuccessStepInput,
    { container }
  ) {
    const workflowEngineService = container.resolve(
      Modules.WORKFLOW_ENGINE
    )

    await workflowEngineService.setStepSuccess({
      idempotencyKey: {
        action: TransactionHandlerType.INVOKE,
        transactionId,
        stepId: "step-2",
        workflowId: "hello-world",
      },
      stepResponse: new StepResponse("Done!"),
    })
  }
)
```

In this step (which you use in a workflow other than the long-running workflow), you resolve the Workflow Engine Module's main service and set `step-2` of the previous workflow as successful.

The `setStepSuccess` method of the workflow engine's main service accepts as a parameter an object having the following properties:

- idempotencyKey: (\`object\`) The details of the workflow execution.

  - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`.

  - transactionId: (\`string\`) The ID of the workflow execution's transaction.

  - stepId: (\`string\`) The ID of the step to change its status. This is the first parameter passed to \`createStep\` when creating the step.

  - workflowId: (\`string\`) The ID of the workflow. This is the first parameter passed to \`createWorkflow\` when creating the workflow.
- stepResponse: (\`StepResponse\`) Set the response of the step. This is similar to the response you return in a step's definition, but since the \`async\` step doesn't have a response, you set its response when changing its status.

### Change Step Status to Failed

The Workflow Engine Module's main service also has a `setStepFailure` method that changes a step's status to failed. It accepts the same parameter as `setStepSuccess`.

After changing the async step's status to failed, the workflow execution fails and the compensation functions of previous steps are executed.

For example:

```ts highlights={failureStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
import {
  Modules,
  TransactionHandlerType,
} from "@medusajs/framework/utils"
import { 
  StepResponse, 
  createStep,
} from "@medusajs/framework/workflows-sdk"

type SetStepFailureStepInput = {
  transactionId: string
};

export const setStepFailureStep = createStep(
  "set-step-failure-step",
  async function (
    { transactionId }: SetStepFailureStepInput,
    { container }
  ) {
    const workflowEngineService = container.resolve(
      Modules.WORKFLOW_ENGINE
    )

    await workflowEngineService.setStepFailure({
      idempotencyKey: {
        action: TransactionHandlerType.INVOKE,
        transactionId,
        stepId: "step-2",
        workflowId: "hello-world",
      },
      stepResponse: new StepResponse("Failed!"),
    })
  }
)
```

You use this step in another workflow that changes the status of an async step in a long-running workflow's execution to failed.

***

## Access Long-Running Workflow Status and Result

To access the status and result of a long-running workflow execution, use the `subscribe` and `unsubscribe` methods of the Workflow Engine Module's main service.

To retrieve the workflow execution's details at a later point, you must enable [storing the workflow's executions](https://docs.medusajs.com/learn/fundamentals/workflows/store-executions/index.html.md).

For example:

```ts title="src/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-11" expandButtonLabel="Show Imports"
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import myWorkflow from "../../../workflows/hello-world"
import { 
  IWorkflowEngineService,
} from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"

export async function GET(req: MedusaRequest, res: MedusaResponse) {
  const { transaction, result } = await myWorkflow(req.scope).run()

  const workflowEngineService = req.scope.resolve<
    IWorkflowEngineService
  >(
    Modules.WORKFLOW_ENGINE
  )

  const subscriptionOptions = {
    workflowId: "hello-world",
    transactionId: transaction.transactionId,
    subscriberId: "hello-world-subscriber",
  }

  await workflowEngineService.subscribe({
    ...subscriptionOptions,
    subscriber: async (data) => {
      if (data.eventType === "onFinish") {
        console.log("Finished execution", data.result)
        // unsubscribe
        await workflowEngineService.unsubscribe({
          ...subscriptionOptions,
          subscriberOrId: subscriptionOptions.subscriberId,
        })
      } else if (data.eventType === "onStepFailure") {
        console.log("Workflow failed", data.step)
      }
    },
  })

  res.send(result)
}
```

In the above example, you execute the long-running workflow `hello-world` and resolve the Workflow Engine Module's main service from the Medusa container.

### subscribe Method

The main service's `subscribe` method allows you to listen to changes in the workflow execution’s status. It accepts an object having three properties:

- workflowId: (\`string\`) The name of the workflow.
- transactionId: (\`string\`) The ID of the workflow exection's transaction. The transaction's details are returned in the response of the workflow execution.
- subscriberId: (\`string\`) The ID of the subscriber.
- subscriber: (\`(data: \{ eventType: string, result?: any }) => Promise\<void>\`) The function executed when the workflow execution's status changes. The function receives a data object. It has an \`eventType\` property, which you use to check the status of the workflow execution.

If the value of `eventType` in the `subscriber` function's first parameter is `onFinish`, the workflow finished executing. The first parameter then also has a `result` property holding the workflow's output.

### unsubscribe Method

You can unsubscribe from the workflow using the workflow engine's `unsubscribe` method, which requires the same object parameter as the `subscribe` method.

However, instead of the `subscriber` property, it requires a `subscriberOrId` property whose value is the same `subscriberId` passed to the `subscribe` method.

***

## Example: Restaurant-Delivery Recipe

To find a full example of a long-running workflow, refer to the [restaurant-delivery recipe](https://docs.medusajs.com/resources/recipes/marketplace/examples/restaurant-delivery/index.html.md).

In the recipe, you use a long-running workflow that moves an order from placed to completed. The workflow waits for the restaurant to accept the order, the driver to pick up the order, and other external actions.


# Multiple Step Usage in Workflows

In this chapter, you'll learn how to use a step multiple times in a workflow.

## Problem Reusing a Step in a Workflow

In some cases, you may need to use a step multiple times in the same workflow.

The most common example is using the `useQueryGraphStep` multiple times in a workflow to retrieve multiple unrelated data, such as customers and products.

Steps must have a unique ID, which you pass as the first parameter of the `createStep` function.

```ts
const useQueryGraphStep = createStep(
  "use-query-graph"
  // ...
)
```

So, when a step is used multiple times in a workflow, it's registered multiple times in the workflow with the same ID, which causes an error.

```ts
const helloWorkflow = createWorkflow(
  "hello",
  () => {
    const { data: products } = useQueryGraphStep({
      entity: "product",
      fields: ["id"],
    })

    // ERROR OCCURS HERE: A STEP HAS THE SAME ID AS ANOTHER IN THE WORKFLOW
    const { data: customers } = useQueryGraphStep({
      entity: "customer",
      fields: ["id"],
    })
  }
)
```

The next section explains how to fix this issue to use the same step multiple times in a workflow.

***

## How to Use a Step Multiple Times in a Workflow?

When you execute a step in a workflow, you can chain a `config` method to it to change the step's config.

Use the `config` method to change a step's ID for a single execution.

For example, this is the correct way to write the example above:

```ts highlights={highlights}
const helloWorkflow = createWorkflow(
  "hello",
  () => {
    const { data: products } = useQueryGraphStep({
      entity: "product",
      fields: ["id"],
    })

    // ✓ No error occurs, the step has a different ID.
    const { data: customers } = useQueryGraphStep({
      entity: "customer",
      fields: ["id"],
    }).config({ name: "fetch-customers" })
  }
)
```

The `config` method accepts an object with a `name` property. Its value is the new ID for the step to use for this execution only.

The first `useQueryGraphStep` usage has the ID `use-query-graph`, and the second `useQueryGraphStep` usage has the ID `fetch-customers`.


# Workflows

In this chapter, you’ll learn about workflows and how to define and execute them.

## What is a Workflow?

In digital commerce you typically have many systems involved in your operations. For example, you may have an ERP system that holds product master data and accounting reports, a CMS system for content, a CRM system for managing customer campaigns, a payment service to process credit cards, and so on. Sometimes you may even have custom built applications that need to participate in the commerce stack. One of the biggest challenges when operating a stack like this is ensuring consistency in the data spread across systems.

Medusa has a built-in durable execution engine to help complete tasks that span multiple systems. You orchestrate your operations across systems in Medusa instead of having to manage it yourself. Other commerce platforms don't have this capability, which makes them a bottleneck to building customizations and iterating quickly.

A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow similar to how you create a JavaScript function.

However, unlike regular functions, workflows:

- Create an internal representation of your steps, allowing you to track them and their progress.
- Support defining roll-back logic for each step, so that you can handle errors gracefully and your data remain consistent across systems.
- Perform long actions asynchronously, giving you control over when a step starts and finishes.

You implement all custom flows within workflows, then execute them from [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), and [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md).

***

## How to Create and Execute a Workflow?

### 1. Create the Steps

A workflow is made of a series of steps. A step is created using `createStep` from the Workflows SDK.

Create the file `src/workflows/hello-world.ts` with the following content:

![Example of workflow file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732866980/Medusa%20Book/workflow-dir-overview_xklukj.jpg)

```ts title="src/workflows/hello-world.ts" highlights={step1Highlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1", 
  async () => {
    return new StepResponse(`Hello from step one!`)
  }
)
```

The `createStep` function accepts the step's unique name as a first parameter, and the step's function as a second parameter.

Steps must return an instance of `StepResponse`, whose parameter is the data to return to the workflow executing the step.

Steps can accept input parameters. For example, add the following to `src/workflows/hello-world.ts`:

```ts title="src/workflows/hello-world.ts" highlights={step2Highlights}
type WorkflowInput = {
  name: string
}

const step2 = createStep(
  "step-2", 
  async ({ name }: WorkflowInput) => {
    return new StepResponse(`Hello ${name} from step two!`)
  }
)
```

This adds another step whose function accepts as a parameter an object with a `name` property.

### 2. Create a Workflow

Next, add the following to the same file to create the workflow using the `createWorkflow` function:

```ts title="src/workflows/hello-world.ts" highlights={workflowHighlights}
import {
  // other imports...
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

// ...

const myWorkflow = createWorkflow(
  "hello-world",
  function (input: WorkflowInput) {
    const str1 = step1()
    // to pass input
    const str2 = step2(input)

    return new WorkflowResponse({
      message: str2,
    })
  }
)

export default myWorkflow
```

The `createWorkflow` function accepts the workflow's unique name as a first parameter, and the workflow's function as a second parameter. The workflow can accept input which is passed as a parameter to the function.

The workflow must return an instance of `WorkflowResponse`, whose parameter is returned to workflow executors.

### 3. Execute the Workflow

You can execute a workflow from different customizations:

- Execute in an API route to expose the workflow's functionalities to clients.
- Execute in a subscriber to use the workflow's functionalities when a commerce operation is performed.
- Execute in a scheduled job to run the workflow's functionalities automatically at a specified repeated interval.

To execute the workflow, invoke it passing the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) as a parameter. Then, use its `run` method:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import myWorkflow from "../../workflows/hello-world"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await myWorkflow(req.scope)
    .run({
      input: {
        name: "John",
      },
    })

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/order-placed.ts" highlights={[["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import myWorkflow from "../workflows/hello-world"

export default async function handleOrderPlaced({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await myWorkflow(container)
    .run({
      input: {
        name: "John",
      },
    })

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "order.placed",
}
```

### Scheduled Job

```ts title="src/jobs/message-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"], ["11"], ["12"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import myWorkflow from "../workflows/hello-world"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await myWorkflow(container)
    .run({
      input: {
        name: "John",
      },
    })

  console.log(result.message)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
};
```

### 4. Test Workflow

To test out your workflow, start your Medusa application:

```bash npm2yarn
npm run dev
```

Then, if you added the API route above, send a `GET` request to `/workflow`:

```bash
curl http://localhost:9000/workflow
```

You’ll receive the following response:

```json title="Example Response"
{
  "message": "Hello John from step two!"
}
```

***

## Access Medusa Container in Workflow Steps

A step receives an object as a second parameter with configurations and context-related properties. One of these properties is the `container` property, which is the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) to allow you to resolve Framework and commerce tools in your application.

For example, consider you want to implement a workflow that returns the total products in your application. Create the file `src/workflows/product-count.ts` with the following content:

```ts title="src/workflows/product-count.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
import {
  createStep,
  StepResponse,
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

const getProductCountStep = createStep(
  "get-product-count", 
  async (_, { container }) => {
    const productModuleService = container.resolve("product")

    const [, count] = await productModuleService.listAndCountProducts()

    return new StepResponse(count)
  }
)

const productCountWorkflow = createWorkflow(
  "product-count",
  function () {
    const count = getProductCountStep()

    return new WorkflowResponse({
      count,
    })
  }
)

export default productCountWorkflow
```

In `getProductCountStep`, you use the `container` to resolve the Product Module's main service. Then, you use its `listAndCountProducts` method to retrieve the total count of products and return it in the step's response. You then execute this step in the `productCountWorkflow`.

You can now execute this workflow in a custom API route, scheduled job, or subscriber to get the total count of products.

Find a full list of the registered resources in the Medusa container and their registration key in [this reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md). You can use these resources in your custom workflows.


# Run Workflow Steps in Parallel

In this chapter, you’ll learn how to run workflow steps in parallel.

## parallelize Utility Function

If your workflow has steps that don’t rely on one another’s results, run them in parallel using `parallelize` from the Workflows SDK.

The workflow waits until all steps passed to the `parallelize` function finish executing before continuing to the next step.

For example:

```ts highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports"
import {
  createWorkflow,
  WorkflowResponse,
  parallelize,
} from "@medusajs/framework/workflows-sdk"
import {
  createProductStep,
  getProductStep,
  createPricesStep,
  attachProductToSalesChannelStep,
} from "./steps"

interface WorkflowInput {
  title: string
}

const myWorkflow = createWorkflow(
  "my-workflow", 
  (input: WorkflowInput) => {
   const product = createProductStep(input)

   const [prices, productSalesChannel] = parallelize(
     createPricesStep(product),
     attachProductToSalesChannelStep(product)
   )

   const refetchedProduct = getProductStep(product.id)

   return new WorkflowResponse(refetchedProduct)
 }
)
```

The `parallelize` function accepts the steps to run in parallel as a parameter.

It returns an array of the steps' results in the same order they're passed to the `parallelize` function.

So, `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`.


# Retry Failed Steps

In this chapter, you’ll learn how to configure steps to allow retry on failure.

## What is a Step Retry?

A step retry is a mechanism that allows a step to be retried automatically when it fails. This is useful for handling transient errors, such as network issues or temporary unavailability of a service.

By default, when a step fails, the workflow execution stops, and the workflow is marked as failed. However, you can configure a step to retry on failure.

When a step fails, you can configure the workflow engine to automatically retry the step a specified number of times before marking the workflow as failed. This can help improve the reliability and resilience of your workflows.

You can also configure the interval between retries, awllowing you to wait for a certain period before attempting the step again. This is useful when the failure is due to a temporary issue that may resolve itself after some time.

For example, if a step captures a payment, you may want to retry it daily until the payment is successful or the maximum number of retries is reached.

***

## Configure a Step’s Retry

By default, when an error occurs in a step, the step and the workflow fail, and the execution stops.

You can configure the step to retry on failure. The `createStep` function can accept a configuration object instead of the step’s name as a first parameter.

For example:

```ts title="src/workflows/hello-world.ts" highlights={[["10"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { 
  createStep, 
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  {
    name: "step-1",
    maxRetries: 2,
  },
  async () => {
    console.log("Executing step 1")

    throw new Error("Oops! Something happened.")
  }
)

const myWorkflow = createWorkflow(
  "hello-world", 
  function () {
  const str1 = step1()

  return new WorkflowResponse({
    message: str1,
  })
})

export default myWorkflow
```

The step’s configuration object accepts a `maxRetries` property, which is a number indicating the number of times a step can be retried when it fails.

When you execute the above workflow, you’ll see the following result in the terminal:

```bash
Executing step 1
Executing step 1
Executing step 1
error:   Oops! Something happened.
Error: Oops! Something happened.
```

The first line indicates the first time the step was executed, and the next two lines indicate the times the step was retried. After that, the step and workflow fail.

### Disable Automatic Retries

By default, a step configured with the `maxRetries` property will be retried automatically when it fails.

You can disable automatic retries by setting the `autoRetry` property to `false` in the step's configuration object. Then, when the step fails, its status will be set to temporary failure, and you'll need to manually retry it using the [Workflow Engine Module Service](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/index.html.md).

The `autoRetry` property is available since [Medusa v2.10.2](https://github.com/medusajs/medusa/releases/tag/v2.10.2).

For example, to disable automatic retries:

```ts title="src/workflows/hello-world.ts" highlights={[["5"]]}
const step1 = createStep(
  {
    name: "step-1",
    maxRetries: 2,
    autoRetry: false, // Disable automatic retries
  },
  async () => {
    console.log("Executing step 1")

    throw new Error("Oops! Something happened.")
  }
)
```

This step will not be retried automatically when it fails. Instead, you'll need to manually retry it, as explained in the [Manually Retry a Step](#manually-retry-a-step) section.

***

## Step Retry Intervals

By default, a step is retried immediately after it fails. To specify a wait time before a step is retried, pass a `retryInterval` property to the step's configuration object. Its value is a number of seconds to wait before retrying the step.

For example:

```ts title="src/workflows/hello-world.ts" highlights={[["5"]]}
const step1 = createStep(
  {
    name: "step-1",
    maxRetries: 2,
    retryInterval: 2, // 2 seconds
  },
  async () => {
    console.log("Executing step 1")

    throw new Error("Oops! Something happened.")
  }
)
```

In this example, if the step fails, it will be retried after two seconds.

### Maximum Retry Interval

The `retryInterval` property's maximum value is [Number.MAX\_SAFE\_INTEGER](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER). So, you can set a very long wait time before the step is retried, allowing you to retry steps after a long period.

For example, to retry a step after a day:

```ts title="src/workflows/hello-world.ts" highlights={[["5"]]}
const step1 = createStep(
  {
    name: "step-1",
    maxRetries: 2,
    retryInterval: 86400, // 1 day
  },
  async () => {
    console.log("Executing step 1")

    throw new Error("Oops! Something happened.")
  }
)
```

In this example, if the step fails, it will be retried after `86400` seconds (one day).

### Interval Changes Workflow to Long-Running

By setting `retryInterval` on a step, a workflow that uses that step becomes a [long-running workflow](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md) that runs asynchronously in the background. This is useful when creating workflows that may fail and should run for a long time until they succeed, such as waiting for a payment to be captured or a shipment to be delivered.

However, since the long-running workflow runs in the background, you won't receive its result or errors immediately when you execute the workflow.

Instead, you must subscribe to the workflow's execution using the Workflow Engine Module's service. Learn more about this in the [Long-Running Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow#access-long-running-workflow-status-and-result/index.html.md) chapter.

***

## Manually Retry a Step

In some cases, you may need to manually retry a step. For example:

- If a step's `autoRetry` property is set to `false`.
- If the machine running the Medusa application in development or [worker mode](https://docs.medusajs.com/learn/configurations/medusa-config#workermode/index.html.md) dies or shuts down.
- If the step takes longer than expected to complete.

To retry a step manually, resolve the Workflow Engine Module's service from the Medusa container and call its `retryStep` method.

The `retryStep` method is available since [Medusa v2.10.2](https://github.com/medusajs/medusa/releases/tag/v2.10.2).

For example, to do it in an API route:

```ts title="src/api/retry-step/route.ts" highlights={retryHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { TransactionHandlerType } from "@medusajs/framework/utils"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const workflowEngine = req.scope.resolve("workflows")

  workflowEngine.retryStep({
    idempotencyKey: {
      action: TransactionHandlerType.INVOKE,
      transactionId: req.validatedQuery.transaction_id as string,
      stepId: "step-1",
      workflowId: "hello-world",
    },
  })

  res.json({ message: "Step retry initiated" })
}
```

When you send a request to the API route, the workflow execution will resume, retrying the specified step.

Learn more about the `retryStep` method in the [Workflow Engine Module Service reference](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/how-to-use#retryStep/index.html.md).


# Store Workflow Executions

In this chapter, you'll learn how to store workflow executions in the database and access them later.

## Workflow Execution Retention

Medusa doesn't store your workflow's execution details by default. However, you can configure a workflow to keep its execution details stored in the database.

This is useful for auditing and debugging purposes. When you store a workflow's execution, you can view details around its steps, their states and their output. You can also check whether the workflow or any of its steps failed.

You can view stored workflow executions from the Medusa Admin dashboard by going to Settings -> Workflows.

***

## How to Store Workflow's Executions?

### Prerequisites

- [Redis Workflow Engine must be installed and configured.](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md)

`createWorkflow` from the Workflows SDK can accept an object as a first parameter to set the workflow's configuration. To enable storing a workflow's executions:

- Enable the `store` option. If your workflow is a [Long-Running Workflow](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md), this option is enabled by default.
- Set the `retentionTime` option to the number of seconds that the workflow execution should be stored in the database.

For example:

```ts highlights={highlights}
import { createStep, createWorkflow } from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  {
    name: "step-1",
  },
  async () => {
    console.log("Hello from step 1")
  }
)

export const helloWorkflow = createWorkflow(
  {
    name: "hello-workflow",
    retentionTime: 99999,
    store: true,
  },
  () => {
    step1()
  }
)
```

Whenever you execute the `helloWorkflow` now, its execution details will be stored in the database.

***

## Retrieve Workflow Executions

You can view stored workflow executions from the Medusa Admin dashboard by going to Settings -> Workflows.

When you execute a workflow, the returned object has a `transaction` property containing the workflow execution's transaction details:

```ts
const { transaction } = await helloWorkflow(container).run()
```

To retrieve a workflow's execution details from the database, resolve the Workflow Engine Module from the container and use its `listWorkflowExecutions` method.

For example, you can create a `GET` API Route at `src/workflows/[id]/route.ts` that retrieves a workflow execution for the specified transaction ID:

```ts title="src/workflows/[id]/route.ts" highlights={retrieveHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { transaction_id } = req.params
  
  const workflowEngineService = req.scope.resolve(
    Modules.WORKFLOW_ENGINE
  )

  const [workflowExecution] = await workflowEngineService.listWorkflowExecutions({
    transaction_id: transaction_id,
  })

  res.json({
    workflowExecution,
  })
}
```

In the above example, you resolve the Workflow Engine Module from the container and use its `listWorkflowExecutions` method, passing the `transaction_id` as a filter to retrieve its workflow execution details.

A workflow execution object will be similar to the following:

```json
{
  "workflow_id": "hello-workflow",
  "transaction_id": "01JJC2T6AVJCQ3N4BRD1EB88SP",
  "id": "wf_exec_01JJC2T6B3P76JD35F12QTTA78",
  "execution": {
    "state": "done",
    "steps": {},
    "modelId": "hello-workflow",
    "options": {},
    "metadata": {},
    "startedAt": 1737719880027,
    "definition": {},
    "timedOutAt": null,
    "hasAsyncSteps": false,
    "transactionId": "01JJC2T6AVJCQ3N4BRD1EB88SP",
    "hasFailedSteps": false,
    "hasSkippedSteps": false,
    "hasWaitingSteps": false,
    "hasRevertedSteps": false,
    "hasSkippedOnFailureSteps": false
  },
  "context": {
    "data": {},
    "errors": []
  },
  "state": "done",
  "created_at": "2025-01-24T09:58:00.036Z",
  "updated_at": "2025-01-24T09:58:00.046Z",
  "deleted_at": null
}
```

### Example: Check if Stored Workflow Execution Failed

To check if a stored workflow execution failed, you can check its `state` property:

```ts
if (workflowExecution.state === "failed") {
  return res.status(500).json({
    error: "Workflow failed",
  })
}
```

Other state values include `done`, `invoking`, and `compensating`.


# Data Manipulation in Workflows with transform

In this chapter, you'll learn how to use `transform` from the Workflows SDK to manipulate data and variables in a workflow.

## Why Variable Manipulation isn't Allowed in Workflows

Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps.

At that point, variables in the workflow don't have any values. They only do when you execute the workflow.

So, you can only pass variables as parameters to steps. But, in a workflow, you can't change a variable's value or, if the variable is an array, loop over its items.

Instead, use `transform` from the Workflows SDK.

Restrictions for variable manipulation is only applicable in a workflow's definition. You can still manipulate variables in your step's code.

***

## What is the transform Utility?

`transform` creates a new variable as the result of manipulating other variables.

For example, consider you have two strings as the output of two steps:

```ts
const str1 = step1()
const str2 = step2()
```

To concatenate the strings, you create a new variable `str3` using the `transform` function:

```ts highlights={highlights}
import { 
  createWorkflow,
  WorkflowResponse,
  transform,
} from "@medusajs/framework/workflows-sdk"
// step imports...

const myWorkflow = createWorkflow(
  "hello-world", 
  function (input) {
    const str1 = step1(input)
    const str2 = step2(input)

    const str3 = transform(
      { str1, str2 },
      (data) => `${data.str1}${data.str2}`
    )

    return new WorkflowResponse(str3)
  }
)
```

`transform` accepts two parameters:

1. The first parameter is an object of variables to manipulate. The object is passed as a parameter to `transform`'s second parameter function.
2. The second parameter is the function performing the variable manipulation.

The value returned by the second parameter function is returned by `transform`. So, the `str3` variable holds the concatenated string.

You can use the returned value in the rest of the workflow, either to pass it as an input to other steps or to return it in the workflow's response.

***

## Example: Looping Over Array

Use `transform` to loop over arrays to create another variable from the array's items.

For example:

```ts collapsibleLines="1-7" expandButtonLabel="Show Imports"
import { 
  createWorkflow,
  WorkflowResponse,
  transform,
} from "@medusajs/framework/workflows-sdk"
// step imports...

type WorkflowInput = {
  items: {
    id: string
    name: string
  }[]
}

const myWorkflow = createWorkflow(
  "hello-world", 
  function ({ items }: WorkflowInput) {
    const ids = transform(
      { items },
      (data) => data.items.map((item) => item.id)
    )
    
    doSomethingStep(ids)

    // ...
  }
)
```

This workflow receives an `items` array in its input.

You use `transform` to create an `ids` variable, which is an array of strings holding the `id` of each item in the `items` array.

You then pass the `ids` variable as a parameter to the `doSomethingStep`.

***

## Example: Creating a Date

If you create a date with `new Date()` in a workflow's constructor function, Medusa evaluates the date's value when it creates the internal representation of the workflow, not when the workflow is executed.

So, use `transform` instead to create a date variable with `new Date()`.

For example:

```ts
const myWorkflow = createWorkflow(
  "hello-world",
  () => {
    const today = transform({}, () => new Date())

    doSomethingStep(today)
  }
)
```

In this workflow, `today` is only evaluated when the workflow is executed.

***

## Caveats

### Transform Evaluation

`transform`'s value is only evaluated if you pass its output to a step or in the workflow response.

For example, if you have the following workflow:

```ts
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input) {
    const str = transform(
      { input },
      (data) => `${data.input.str1}${data.input.str2}`
    )

    return new WorkflowResponse("done")
  }
)
```

Since `str`'s value isn't used as a step's input or passed to `WorkflowResponse`, its value is never evaluated.

### Data Validation

`transform` should only be used to perform variable or data manipulation.

If you want to perform some validation on the data, use a step or [when-then](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md) instead.

For example:

```ts
// DON'T
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input) {
    const str = transform(
      { input },
      (data) => {
        if (!input.str1) {
          throw new Error("Not allowed!")
        }
      }
    )
  }
)

// DO
const validateHasStr1Step = createStep(
  "validate-has-str1",
  ({ input }) => {
    if (!input.str1) {
      throw new Error("Not allowed!")
    }
  }
)

const myWorkflow = createWorkflow(
  "hello-world", 
  function (input) {
    validateHasStr1({
      input,
    })

    // workflow continues its execution only if 
    // the step doesn't throw the error.
  }
)
```


# Workflow Hooks

In this chapter, you'll learn what workflow hooks are and how to use them.

## What is a Workflow Hook?

A workflow hook is a specific point in a workflow where you can inject custom functionality. This custom functionality is called a hook handler.

Medusa exposes hooks in many of its workflows that are used in its API routes. You can consume those hooks to add your custom logic.

Refer to the [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) to view all workflows and their hooks.

You want to perform a custom action during a workflow's execution, such as when a product is created.

***

## How to Consume a Hook?

A workflow has a special `hooks` property. This property is an object that contains all available hooks.

So, in a TypeScript or JavaScript file created under the `src/workflows/hooks` directory:

1. Import the workflow.
2. Access the hook using the `hooks` property.
3. Pass a step function as a parameter to the hook.

For example, to consume the `productsCreated` hook of Medusa's `createProductsWorkflow`, create the file `src/workflows/hooks/product-created.ts` with the following content:

```ts title="src/workflows/hooks/product-created.ts" highlights={handlerHighlights}
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"

createProductsWorkflow.hooks.productsCreated(
  async ({ products }, { container }) => {
    // TODO perform an action
  }
)
```

The `productsCreated` hook is available in the workflow's `hooks` property.

You call the hook and pass a step function (the hook handler) as a parameter.

Now, when a product is created using the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts), your hook handler runs after the product is created.

A hook can have only one handler. So, you can't consume the same hook multiple times.

Refer to the [createProductsWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md) to see at which point the hook handler is executed.

### Hook Handler Parameter

Since a hook handler is essentially a step function, it receives the hook's input as a first parameter, and an object holding a `container` property as a second parameter.

Each hook has different input. For example, the `productsCreated` hook receives an object with a `products` property that contains the created product.

You can find the input for each workflow's hooks in the [Core Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md).

### Hook Handler Compensation

Since the hook handler is a step function, you can set its compensation function as a second parameter of the hook.

For example:

```ts title="src/workflows/hooks/product-created.ts"
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"

createProductsWorkflow.hooks.productsCreated(
  async ({ products }, { container }) => {
    // TODO perform an action

    return new StepResponse(undefined, { ids })
  },
  async ({ ids }, { container }) => {
    // undo the performed action
  }
)
```

The compensation function runs if an error occurs in the workflow. It undoes the actions performed by the hook handler.

The compensation function receives the second parameter passed to the `StepResponse` returned by the step function as input.

It also accepts an object with a `container` property as a second parameter. This allows you to resolve resources from the Medusa container.

### Additional Data Property

Medusa's workflows include an `additional_data` property in the hook's input:

```ts title="src/workflows/hooks/product-created.ts" highlights={[["4", "additional_data"]]}
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"

createProductsWorkflow.hooks.productsCreated(
  async ({ products, additional_data }, { container }) => {
    // TODO perform an action
  }
)
```

This property is an object that contains additional data passed to the workflow through the request sent to the API route.

Learn how to pass `additional_data` in requests to API routes in the [Additional Data](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md) chapter.

### Pass Additional Data to Workflow

You can also pass additional data when running the workflow. Pass it as a parameter to the workflow's `.run` method:

```ts title="src/workflows/hooks/product-created.ts" highlights={[["10", "additional_data"]]}
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"

export async function POST(req: MedusaRequest, res: MedusaResponse) {
  await createProductsWorkflow(req.scope).run({
    input: { 
      products: [
        // ...
      ], 
      additional_data: {
        custom_field: "test",
      },
    },
  })
}
```

Your hook handler then receives the passed data in the `additional_data` object.


# Workflow Timeout

In this chapter, you’ll learn how to set a timeout for workflows and steps.

## What is a Workflow Timeout?

By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs.

You can configure a workflow’s timeout to indicate how long the workflow can execute. If a workflow's execution time passes the configured timeout, it is failed and an error is thrown.

### Timeout Doesn't Stop Step Execution

Configuring a timeout doesn't stop the execution of a step in progress. The timeout only affects the status of the workflow and its result.

***

## Configure Workflow Timeout

The `createWorkflow` function can accept a configuration object instead of the workflow’s name.

In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds.

For example:

```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More"
import { 
  createStep,  
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async () => {
    // ...
  }
)

const myWorkflow = createWorkflow({
  name: "hello-world",
  timeout: 2, // 2 seconds
}, function () {
  const str1 = step1()

  return new WorkflowResponse({
    message: str1,
  })
})

export default myWorkflow

```

This workflow's executions fail if they run longer than two seconds.

A workflow’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). The error’s name is `TransactionTimeoutError`.

***

## Configure Step Timeout

Alternatively, you can configure the timeout for a step rather than the entire workflow.

As mentioned in the previous section, the timeout doesn't stop the execution of the step. It only affects the step's status and output.

The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds.

For example:

```tsx
const step1 = createStep(
  {
    name: "step-1",
    timeout: 2, // 2 seconds
  },
  async () => {
    // ...
  }
)
```

This step's executions fail if they run longer than two seconds.

A step’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). The error’s name is `TransactionStepTimeoutError`.


# Install Medusa with Docker

In this chapter, you'll learn how to install and run a Medusa application using Docker.

The main supported installation method is using [create-medusa-app](https://docs.medusajs.com/learn/installation/index.html.md). However, since it requires prerequisites like PostgreSQL, Docker may be a preferred and easier option for some users. You can follow this guide to set up a Medusa application with Docker in this case.

You can follow this guide on any operating system that supports Docker, including Windows, macOS, and Linux.

### Prerequisites

- [Docker](https://docs.docker.com/get-docker/)
- [Docker Compose](https://docs.docker.com/compose/install/)
- [Git CLI tool](https://git-scm.com/downloads)

## 1. Clone Medusa Starter Repository

To get started, clone the Medusa Starter repository that a Medusa application is based on:

```bash
git clone https://github.com/medusajs/medusa-starter-default.git --depth=1 my-medusa-store
```

This command clones the repository into a directory named `my-medusa-store`.

Make sure to change into the newly created directory:

```bash
cd my-medusa-store
```

The rest of the steps will be performed inside this directory.

***

## 2. Create `docker-compose.yml`

In the cloned repository, create a file named `docker-compose.yml` with the following content:

```yaml title="docker-compose.yml"
services:
  # PostgreSQL Database
  postgres:
    image: postgres:15-alpine
    container_name: medusa_postgres
    restart: unless-stopped
    environment:
      POSTGRES_DB: medusa-store
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - medusa_network

  # Redis
  redis:
    image: redis:7-alpine
    container_name: medusa_redis
    restart: unless-stopped
    ports:
      - "6379:6379"
    networks:
      - medusa_network

  # Medusa Server
  # This service runs the Medusa backend application
  # and the admin dashboard.
  medusa:
    build: .
    container_name: medusa_backend
    restart: unless-stopped
    depends_on:
      - postgres
      - redis
    ports:
      - "9000:9000"
      - "5173:5173"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgres://postgres:postgres@postgres:5432/medusa-store
      - REDIS_URL=redis://redis:6379
    env_file:
      - .env
    volumes:
      - .:/server
      - /server/node_modules
    networks:
      - medusa_network

volumes:
  postgres_data:

networks:
  medusa_network:
    driver: bridge
```

You define three services in this file:

- `postgres`: The PostgreSQL database service that stores your Medusa application's data.
- `redis`: The Redis service that stores session data.
- `medusa`: The Medusa service that runs the server and the admin dashboard. It connects to the PostgreSQL and Redis services.

You can add environment variables either in the `environment` section of the `medusa` service or in a separate `.env` file.

### Recommendations for Multiple Local Projects

If this isn't the first Medusa project you're setting up with Docker on your machine, make sure to:

- Change the `container_name` for each service to avoid conflicts.
  - For example, use `medusa_postgres_myproject`, `medusa_redis_myproject`, and `medusa_backend_myproject` instead of the current names.
- Change the volume names to avoid conflicts.
  - For example, use `postgres_data_myproject` instead of `postgres_data`.
- Change the network name to avoid conflicts.
  - For example, use `medusa_network_myproject` instead of `medusa_network`.
- Change the ports to avoid conflicts with other projects. For example:
  - Use `"5433:5432"` for PostgreSQL.
  - Use `"6380:6379"` for Redis.
  - Use `"9001:9000"` for the Medusa server.
  - Use `"5174:5173"` for the Medusa Admin dashboard.
- Update the `DATABASE_URL` and `REDIS_URL` environment variables accordingly.

***

## 3. Create `start.sh`

Next, you need to create a script file that [runs database migrations](https://docs.medusajs.com/learn/fundamentals/data-models/write-migration/index.html.md) and starts the Medusa development server.

Create the file `start.sh` with the following content:

Ensure that the `start.sh` file uses LF line endings instead of CRLF. Git on Windows can sometimes automatically convert line endings, which causes errors when running the script inside the Linux-based Docker container. Learn how to configure your environment to maintain LF line endings in [this guide from GitHub](https://docs.github.com/en/get-started/git-basics/configuring-git-to-handle-line-endings).

### Yarn

```shell title="start.sh"
#!/bin/sh

# Run migrations and start server
echo "Running database migrations..."
npx medusa db:migrate

echo "Seeding database..."
yarn seed || echo "Seeding failed, continuing..."

echo "Starting Medusa development server..."
yarn dev
```

### NPM

```shell title="start.sh"
#!/bin/sh

# Run migrations and start server
echo "Running database migrations..."
npx medusa db:migrate

echo "Seeding database..."
npm run seed || echo "Seeding failed, continuing..."

echo "Starting Medusa development server..."
npm run dev
```

Make sure to give the script executable permissions:

Setting permissions isn't necessary on Windows, but it's recommended to run this command on Unix-based systems like macOS and Linux.

```bash
chmod +x start.sh
```

***

## 4. Create `Dockerfile`

The `Dockerfile` defines how the Medusa service is built.

Create a file named `Dockerfile` with the following content:

### Yarn

```dockerfile title="Dockerfile"
# Development Dockerfile for Medusa
FROM node:20-alpine

# Set working directory
WORKDIR /server

# Copy package files and yarn config
COPY package.json yarn.lock .yarnrc.yml ./

# Install all dependencies using yarn
RUN yarn install

# Copy source code
COPY . .

# Expose the port Medusa runs on
EXPOSE 9000

# Start with migrations and then the development server
CMD ["./start.sh"]
```

### NPM

```dockerfile title="Dockerfile"
# Development Dockerfile for Medusa
FROM node:20-alpine

# Set working directory
WORKDIR /server

# Copy package files and npm config
COPY package.json package-lock.json ./

# Install all dependencies using npm
RUN npm install --legacy-peer-deps

# Copy source code
COPY . .

# Expose the port Medusa runs on
EXPOSE 9000

# Start with migrations and then the development server
CMD ["./start.sh"]
```

In the `Dockerfile`, you use the `node:20-alpine` image as the base since Medusa requires Node.js v20 or later.

Then, you set the working directory to `/server`, copy the necessary files, install dependencies, expose the `9000` port that Medusa uses, and run the `start.sh` script to start the server.

While it's more common to use `/app` as the working directory, it's highly recommended to use `/server` for the Medusa service to avoid conflicts with Medusa Admin customizations. Learn more in [this troubleshooting guide](https://docs.medusajs.com/resources/troubleshooting/medusa-admin/no-widget-route#errors-in-docker/index.html.md).

***

## 5. Install Dependencies

The Medusa Starter repository has a `yarn.lock` file that was generated by installing dependencies with Yarn v1.22.19.

If you're using a different Yarn version, or you're using NPM, you need to install the dependencies again to ensure compatibility with the Docker setup.

To install the dependencies, run the following command:

### npm

```bash
npm install --legacy-peer-deps
```

### yarn

```bash
yarn install
```

### pnpm

```bash
pnpm install
```

This will update `yarn.lock` or generate a `package-lock.json` file, depending on your package manager.

***

## 6. Update Scripts in `package.json`

Next, you need to update the `scripts` section in your `package.json` file to start the development server using Docker.

Add the following scripts in `package.json`:

```json title="package.json"
{
  "scripts": {
    // Other scripts...
    "docker:up": "docker compose up --build -d",
    "docker:down": "docker compose down"
  }
}
```

Where:

- `docker:up` starts the development server in a Docker container as a background process.
- `docker:down` stops and removes the Docker containers.

***

## 7. Update Medusa Configuration

### Disable SSL for PostgreSQL Connection

If you try to run the Medusa application now with Docker, you'll encounter an SSL error as the server tries to connect to the PostgreSQL database.

To resolve the error, add the following configurations in `medusa-config.ts`:

```ts title="medusa-config.ts"
import { loadEnv, defineConfig } from "@medusajs/framework/utils"

loadEnv(process.env.NODE_ENV || "development", process.cwd())

module.exports = defineConfig({
  projectConfig: {
    // ...
    databaseDriverOptions: {
      ssl: false,
      sslmode: "disable",
    },
  },
})
```

You add the [projectConfig.databaseDriverOptions](https://docs.medusajs.com/learn/configurations/medusa-config#databasedriveroptions/index.html.md) to disable SSL for the PostgreSQL database connection.

### Add Vite Configuration for Medusa Admin

To ensure that the Medusa Admin dashboard works correctly when running inside Docker, connects to the Medusa server, and reloads properly with Hot Module Replacement (HMR), add the following Vite configuration in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  admin: {
    vite: (config) => {
      return {
        ...config,
        server: {
          ...config.server,
          host: "0.0.0.0",
          // Allow all hosts when running in Docker (development mode)
          // In production, this should be more restrictive
          allowedHosts: [
            "localhost",
            ".localhost",
            "127.0.0.1",
          ],
          hmr: {
            ...config.server?.hmr,
            // HMR websocket port inside container
            port: 5173, 
            // Port browser connects to (exposed in docker-compose.yml)
            clientPort: 5173,
          },
        },
      }
    },
  },
})
```

You configure the Vite development server to listen on all network interfaces (`0.0.0.0`) and set up HMR to work correctly within the Docker environment.

***

## 8. Add `.dockerignore`

To ensure only the necessary files are copied into the Docker image, create a `.dockerignore` file with the following content:

```dockerignore title=".dockerignore"
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.git
.gitignore
README.md
.env.test
.nyc_output
coverage
.DS_Store
*.log
dist
build
```

***

## 9. Create `.env` File

You can add environment variables either in the `environment` section of the `medusa` service in `docker-compose.yml` or in a separate `.env` file.

If you don't want to use a `.env` file, you can remove the `env_file` section from the `medusa` service in `docker-compose.yml`.

Otherwise, copy the `.env.template` file to `.env` and update the values as needed.

***

## 10. Start the Medusa Application with Docker

All configurations are now ready. You can start the Medusa application using Docker by running the following command:

```bash npm2yarn
npm run docker:up
```

Docker will pull the necessary images, start the PostgreSQL and Redis services, build the Medusa service, and run the development server in a Docker container.

You can check the logs to ensure everything is running smoothly with the following command:

```bash
docker compose logs -f
```

Once you see the following message, the Medusa server and admin are ready:

```shell
✔ Server is ready on port: 9000 – 3ms
info:    Admin URL → http://localhost:9000/app
```

You can now access the Medusa server at `http://localhost:9000` and the Medusa Admin dashboard at `http://localhost:9000/app`.

***

## Create Admin User

To create an admin user, run the following command:

```bash
docker compose run --rm medusa npx medusa user -e admin@example.com -p supersecret
```

Make sure to replace `admin@example.com` and `supersecret` with your desired email and password.

You can now log in to the Medusa Admin dashboard at `http://localhost:9000/app` using the email and password you just created.

***

## Stop the Medusa Application Running in Docker

To stop the Medusa application running in Docker, run the following command:

```bash npm2yarn
npm run docker:down
```

This command stops and removes the Docker containers created by the `docker-compose.yml` file.

This doesn't delete any data in your application or its database. You can start the server again using the `docker:up` command.

***

## Check Logs

You can check the logs of the Medusa application running in Docker using the following command:

```bash
docker compose logs -f medusa
```

This command shows the logs of the `medusa` service, allowing you to see any errors or messages from the Medusa application.

***

## Troubleshooting

### start.sh Not Found Error

If you get the following error when starting the Medusa application with Docker:

```bash
medusa_backend exited with code 127 (restarting)
medusa_backend   | /usr/local/bin/docker-entrypoint.sh: exec: line 11: ./start.sh: not found
```

This is a common error for Windows users. It usually occurs when the `start.sh` file uses CRLF line endings instead of LF.

To fix this, ensure that the `start.sh` file uses LF line endings. You can configure Git to maintain LF line endings by following [this guide from GitHub](https://docs.github.com/en/get-started/git-basics/configuring-git-to-handle-line-endings).

### Couldn't Find X File or Directory Errors

If you encounter errors indicating that certain files or directories couldn't be found, make sure you're running the commands from the root directory of your Medusa application (the same directory where the `docker-compose.yml` file is located).

For example, if you run the `docker:up` command and see an error like this:

```bash
error Couldn't find a package.json file in "/"
```

Ensure that your terminal's current working directory is the root of your Medusa application, then try running the command again.

### Container Name Conflicts

If you're running multiple Medusa projects with Docker or have previously run this guide, you may encounter container name conflicts.

To resolve this, ensure that the `container_name` values in your `docker-compose.yml` file are unique for each project. You can modify them by appending a unique identifier to each name.

For example, change:

```yaml title="docker-compose.yml"
container_name: medusa_postgres
```

to

```yaml title="docker-compose.yml"
container_name: medusa_postgres_myproject
```

***

## Learn More about your Medusa Application

You can learn more about your Medusa application and its setup in the [Installation chapter](https://docs.medusajs.com/learn/installation/index.html.md).


# Install Medusa

In this chapter, you'll learn how to install and run a Medusa application.

## Get Started with Cloud

[Cloud](https://docs.medusajs.com/cloud/index.html.md) is Medusa's PaaS platform that allows you to deploy and manage production-ready Medusa applications with ease. Benefit from features like zero-configuration deployments, automatic scaling, storefront hosting, and GitHub integration to streamline your development workflow.

[Sign up](http://cloud.medusajs.com/signup) with Cloud and create your first Medusa project in minutes.

***

## Create Medusa Application Locally

A Medusa application is made up of a Node.js server and a Vite admin dashboard. You can optionally install the [Next.js Starter Storefront](https://docs.medusajs.com/resources/nextjs-starter/index.html.md) separately either while installing the Medusa application or at a later point.

While this is the recommended way to create a Medusa application, you can alternatively [install a Medusa application with Docker](https://docs.medusajs.com/learn/installation/docker/index.html.md).

### Prerequisites

- [Node.js v20+ (LTS versions only)](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL installed and running](https://www.postgresql.org/download/)

To create a Medusa application, use the `create-medusa-app` command:

```bash
npx create-medusa-app@latest my-medusa-store
```

Where `my-medusa-store` is the name of the project's directory and PostgreSQL database created for the project. When you run the command, you'll be asked whether you want to install the Next.js Starter Storefront.

If you choose to install the Next.js Starter Storefront, note that it currently doesn't support Node v25+. Make sure to downgrade to Node v24 LTS or lower before installing the Medusa application with the storefront.

To customize the default installation behavior, such as specify a database URL, refer to the [create-medusa-app reference](https://docs.medusajs.com/resources/create-medusa-app/index.html.md).

After answering the prompts, the command installs the Medusa application in a directory with your project name, and sets up a PostgreSQL database that the application connects to.

If you chose to install the storefront with the Medusa application, the storefront is installed in a separate directory named `{project-name}-storefront`.

![Directory structure overview after Medusa installation showing the main project folder containing the Medusa backend application and admin dashboard, alongside the separate storefront directory for the customer-facing Next.js application](https://res.cloudinary.com/dza7lstvk/image/upload/v1745856132/Medusa%20Resources/installation-dirs_x8jux4.jpg)

### Successful Installation Result

Once the installation finishes successfully, the Medusa application will run at `http://localhost:9000`.

The Medusa Admin dashboard also runs at `http://localhost:9000/app`. The installation process opens the Medusa Admin dashboard in your default browser to create a user. You can later log in with that user.

If you also installed the Next.js Starter Storefront, it'll be running at `http://localhost:8000`.

You can stop the servers for the Medusa application and Next.js Starter Storefront by exiting the installation command. To run the server for the Medusa application again, refer to [this section](#run-medusa-application-in-development).

![Post-installation running services overview: Medusa backend server and admin dashboard running on localhost:9000, Next.js Starter Storefront running on localhost:8000, with PostgreSQL database and other essential services active and ready for development](https://res.cloudinary.com/dza7lstvk/image/upload/v1745856706/Medusa%20Resources/success-overview_bj4pbt.jpg)

### Troubleshooting Installation Errors

If you ran into an error during your installation, refer to the following troubleshooting guides for help:

1. [create-medusa-app troubleshooting guides](https://docs.medusajs.com/resources/troubleshooting/create-medusa-app-errors/index.html.md).
2. [CORS errors](https://docs.medusajs.com/resources/troubleshooting/cors-errors/index.html.md).
3. [Errors with pnpm](https://docs.medusajs.com/resources/troubleshooting/pnpm/index.html.md).
4. [All troubleshooting guides](https://docs.medusajs.com/resources/troubleshooting/index.html.md).

If you can't find your error reported anywhere, please open a [GitHub issue](https://github.com/medusajs/medusa/issues/new/choose).

***

## Run Medusa Application in Development

To run the Medusa application in development, change to your application's directory and run the following command:

```bash npm2yarn
npm run dev
```

This runs your Medusa server at `http://localhost:9000`, and the Medusa Admin dashboard `http://localhost:9000/app`.

![Diagram showcasing the server and application running when you start the Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1745856966/Medusa%20Resources/start-overview_aetplx.jpg)

For details on starting and configuring the Next.js Starter Storefront, refer to [this documentation](https://docs.medusajs.com/resources/nextjs-starter/index.html.md).

The application will restart if you make any changes to code under the `src` directory, except for admin customizations which are hot reloaded, providing you with a seamless developer experience without having to refresh your browser to see the changes.

***

## Create Medusa Admin User

Aside from creating an admin user in the admin dashboard, you can create a user with Medusa's CLI tool.

Run the following command in your Medusa application's directory to create a new admin user:

```bash
npx medusa user -e admin@medusajs.com -p supersecret
```

Replace `admin@medusajs.com` and `supersecret` with the user's email and password respectively.

You can then use the user's credentials to log into the Medusa Admin application.

***

## Project Files

Your Medusa application's project will have the following files and directories:

![A diagram of the directories overview](https://res.cloudinary.com/dza7lstvk/image/upload/v1732803813/Medusa%20Book/medusa-dir-overview_v7ks0j.jpg)

### src

This directory is the central place for your custom development. It includes the following sub-directories:

- `admin`: Holds your admin dashboard's custom [widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md) and [UI routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md).
- `api`: Holds your custom [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) that are added as endpoints in your Medusa application.
- `jobs`: Holds your [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) that run at a specified interval during your Medusa application's runtime.
- `links`: Holds your [module links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) that build associations between data models of different modules.
- `modules`: Holds your custom [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) that implement custom business logic.
- `scripts`: Holds your custom [scripts](https://docs.medusajs.com/learn/fundamentals/custom-cli-scripts/index.html.md) to be executed using Medusa's CLI tool.
- `subscribers`: Holds your [event listeners](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) that are executed asynchronously whenever an event is emitted.
- `workflows`: Holds your custom [flows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that can be executed from anywhere in your application.

### medusa-config.ts

This file holds your [Medusa configurations](https://docs.medusajs.com/learn/configurations/medusa-config/index.html.md), such as your PostgreSQL database configurations.

### .medusa

The `.medusa` directory holds types and other files that are generated by Medusa when you run the `build` command. Don't modify any files or commit them to your repository.

***

## Configure Medusa Application

By default, your Medusa application is equipped with the basic configuration to start your development.

If you run into issues with configurations, such as CORS configurations, or need to make changes to the default configuration, refer to [this guide on all available configurations](https://docs.medusajs.com/learn/configurations/medusa-config/index.html.md).

***

## Update Medusa Application

Refer to [this documentation](https://docs.medusajs.com/learn/update/index.html.md) to learn how to update your Medusa project.

***

## Next Steps

In the next chapters, you'll learn about the architecture of your Medusa application, then learn how to customize your application to build custom features.


# Medusa's Architecture

In this chapter, you'll learn about the architectural layers in Medusa.

Find the full architectural diagram at the [end of this chapter](#full-diagram-of-medusas-architecture).

## HTTP, Workflow, and Module Layers

Medusa is a headless commerce platform. So, storefronts, admin dashboards, and other clients consume Medusa's functionalities through its API routes.

In a common Medusa application, requests go through four layers in the stack. In order of entry, those are:

1. API Routes (HTTP): Our API Routes are the typical entry point. The Medusa server is based on Express.js, which handles incoming requests. It can also connect to a Redis database that stores the server session data.
2. Workflows: API Routes consume workflows that hold the opinionated business logic of your application.
3. Modules: Workflows use domain-specific modules for resource management.
4. Data store: Modules query the underlying datastore, which is a PostgreSQL database in common cases.

These layers of stack can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md).

![Medusa application architecture diagram illustrating the HTTP layer flow: External clients (storefront and admin) send requests to API routes, which execute workflows containing business logic, which then interact with modules to perform data operations on PostgreSQL databases](https://res.cloudinary.com/dza7lstvk/image/upload/v1759761784/Medusa%20Book/http-layer-new_hu0r3h.jpg)

***

## Database Layer

The Medusa application injects into each module, including your [custom modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), a connection to the configured PostgreSQL database. Modules use that connection to read and write data to the database.

Medusa only supports PostgreSQL as the underlying database. You can also integrate other databases in a [custom module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). For example, the [Loader chapter](https://docs.medusajs.com/learn/fundamentals/modules/loaders#example-register-custom-mongodb-connection/index.html.md) shows how to connect to a MongoDB database through a custom module.

Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md).

![Database layer architecture diagram showing how Medusa modules establish connections to PostgreSQL databases through injected database connections, enabling data persistence and retrieval operations](https://res.cloudinary.com/dza7lstvk/image/upload/v1759761866/Medusa%20Book/db-layer-new_faeksx.jpg)

***

## Third-Party Integrations Layer

Third-party services and systems are integrated through Medusa's Commerce and Infrastructure Modules. You also create custom third-party integrations through a [custom module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md).

Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md).

### Commerce Modules

[Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md) integrate third-party services relevant for commerce or user-facing features. For example, you can integrate [Stripe](https://docs.medusajs.com/resources/commerce-modules/payment/payment-provider/stripe/index.html.md) through a Payment Module Provider, or [ShipStation](https://docs.medusajs.com/resources/integrations/guides/shipstation/index.html.md) through a Fulfillment Module Provider.

You can also integrate third-party services for custom functionalities. For example, you can integrate [Sanity](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md) for rich CMS capabilities, or [Odoo](https://docs.medusajs.com/resources/recipes/erp/odoo/index.html.md) to sync your Medusa application with your ERP system.

You can replace any of the third-party services mentioned above to build your preferred commerce ecosystem.

![Diagram illustrating the Commerce Modules integration to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175357/Medusa%20Book/service-commerce_qcbdsl.jpg)

### Infrastructure Modules

[Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) integrate third-party services and systems that customize Medusa's infrastructure. Medusa has the following Infrastructure Modules:

- [Analytics Module](https://docs.medusajs.com/resources/infrastructure-modules/analytics/index.html.md): Tracks and analyzes user interactions and system events with third-party analytic providers. You can integrate [PostHog](https://docs.medusajs.com/resources/infrastructure-modules/analytics/posthog/index.html.md) as the analytics provider.
- [Caching Module](https://docs.medusajs.com/resources/infrastructure-modules/caching/index.html.md): Caches data that require heavy computation. You can integrate a custom module to handle the caching with services like Memcached, or use the existing [Redis Caching Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/caching/providers/redis/index.html.md).
- [Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/index.html.md): A pub/sub system that allows you to subscribe to events and trigger them. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md) as the pub/sub system.
- [File Module](https://docs.medusajs.com/resources/infrastructure-modules/file/index.html.md): Manages file uploads and storage, such as upload of product images. You can integrate [AWS S3](https://docs.medusajs.com/resources/infrastructure-modules/file/s3/index.html.md) for file storage.
- [Locking Module](https://docs.medusajs.com/resources/infrastructure-modules/locking/index.html.md): Manages access to shared resources by multiple processes or threads, preventing conflict between processes and ensuring data consistency. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/locking/redis/index.html.md) for locking.
- [Notification Module](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md): Sends notifications to customers and users, such as for order updates or newsletters. You can integrate [SendGrid](https://docs.medusajs.com/resources/infrastructure-modules/notification/sendgrid/index.html.md) for sending emails.
- [Workflow Engine Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/index.html.md): Orchestrates workflows that hold the business logic of your application. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md) to orchestrate workflows.

All of the third-party services mentioned above can be replaced to help you build your preferred architecture and ecosystem.

The Caching Module was introduced in [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0) to replace the deprecated Cache Module.

![Diagram illustrating the Infrastructure Modules integration to third-party services and systems](https://res.cloudinary.com/dza7lstvk/image/upload/v1759762284/Medusa%20Book/service-infra_k3fcy0.jpg)

***

## Full Diagram of Medusa's Architecture

The following diagram illustrates Medusa's architecture including all its layers.

![Complete Medusa architecture overview showing the full technology stack: client applications (storefront and admin) connecting through HTTP layer to workflows, which coordinate with commerce and Infrastructure Modules to manage data operations, third-party integrations, and database persistence](https://res.cloudinary.com/dza7lstvk/image/upload/v1759762329/Medusa%20Book/diagram-full-new_znssjy.jpg)


# Build with AI Assistants and LLMs

In this chapter, you'll learn how you can use AI assistants and LLMs effectively in your Medusa development.

## MCP Remote Server

The Medusa documentation provides a remote Model Context Protocol (MCP) server that allows you to find information from the Medusa documentation right in your IDEs or AI tools, such as Cursor.

Medusa hosts a Streamable HTTP MCP server available at `https://docs.medusajs.com/mcp`. you can add it to AI agents that support connecting to MCP servers.

### Cursor

<Link href="cursor://anysphere.cursor-deeplink/mcp/install?name=medusa&config=eyJ1cmwiOiJodHRwczovL2RvY3MubWVkdXNhanMuY29tL21jcCJ9" target="_blank" rel="noopener noreferrer" variant="content">Click here</Link> to add the Medusa MCP server to Cursor.

To manually connect to the Medusa MCP server in Cursor, add the following to your `.cursor/mcp.json` file or Cursor settings, as explained in the [Cursor documentation](https://docs.cursor.com/context/model-context-protocol):

```json
{
  "mcpServers": {
    "medusa": {
      "url": "https://docs.medusajs.com/mcp"
    }
  }
}
```

### VSCode

To manually connect to the Medusa MCP server in Claude Code, run the following command in your terminal:

```sh
claude mcp add --transport http medusa https://docs.medusajs.com/mcp
```

### Claude Code

### How to Use the MCP Server

After connecting to the Medusa MCP server in your AI tool or IDE, you can start asking questions or requesting your AI assistant to build Medusa customizations. It will fetch the relevant information from the Medusa documentation and provide you with accurate answers, code snippets, and explanations.

For example, you can ask:

1. "Create a Product Review module for Medusa. Refer to the Medusa documentation for information."
2. "How to update Medusa to the latest version?"
3. "Explain the Medusa workflow system."
4. "Integrate Medusa with X provider. Refer to the Medusa documentation for information."

***

## AI Assistant in Documentation

The Medusa documentation is equipped with an AI Assistant that can answer your questions and help you build customizations with Medusa.

### Open the AI Assistant

To open the AI Assistant, either:

- Use the keyboard shortcut `Ctrl + I` for Windows/Linux, or `Cmd + I` for macOS.
- Click the <InlineIcon Icon={AiAssistent} alt="AI Assistant" /> icon in the top right corner of the documentation.

You can then ask the AI Assistant any questions about Medusa, such as:

- What is a workflow?
- How to create a product review module?
- How to update Medusa?
- How to fix this error?

The AI Assistant will provide you with relevant documentation links, code snippets, and explanations to help you with your development.

### Ask About Code Snippets

While browsing the documentation, you'll find a <InlineIcon Icon={AiAssistentLuminosity} alt="AI Assistant" /> icon in the header of code snippets. You can click this icon to ask the AI Assistant about the code snippet.

The AI Assistant will analyze the code and provide explanations, usage examples, and any additional information you need to understand how the code works.

### Ask About Documentation Pages

If you need more help understanding a specific documentation page, you can click the "Explain with AI Assistant" link in the page's right sidebar. This will open the AI Assistant and provide context about the current page, allowing you to ask questions related to the content.

### Formatting and Code Blocks

In your questions to the AI Assistant, you can format code blocks by wrapping them in triple backticks (\`\`\`). For example:

````markdown
```
console.log("Hello, World!")
```
````

You can add new lines using the `Shift + Enter` shortcut.

***

## Plain Text Documentation

The Medusa documentation is available in plain text format, which allows LLMs and AI tools to easily parse and understand the content.

You can access the following plain text documentation files:

- [llms.txt](https://docs.medusajs.com/llms.txt/index.html.md) - Contains a short structure of links to important documentation pages.
- [llms-full.txt](https://docs.medusajs.com/llms-full.txt/index.html.md) - Contains the full documentation content, including all pages and sections.
- **Markdown version of any page** - You can access the Markdown version of any documentation page by appending `/index.html.md` to the page URL. For example, the plain text content of the current page is available at [https://docs.medusajs.com/learn/introduction/build-with-llms-ai/index.html.md](https://docs.medusajs.com/learn/introduction/build-with-llms-ai/index.html.md).

You can provide these files to your AI tools or LLM editors like [Cursor](https://docs.cursor.com/context/@-symbols/@-docs). This will help them understand the Medusa documentation and provide better assistance when building customizations or answering questions.


# From Medusa v1 to v2: Conceptual Differences

In this chapter, you'll learn about the differences and changes between concepts in Medusa v1 to v2.

## What to Expect in This Chapter

This chapter is designed to help developers migrate from Medusa v1 to v2 by understanding the conceptual differences between the two versions.

This chapter will cover:

- The general steps to update your project from Medusa v1 to v2.
- The changes in tools and plugins between Medusa v1 and v2.
- The high-level changes in the concepts and commerce features between Medusa v1 and v2.

By following this chapter, you'll learn about the general changes you need to make in your project, with links to read more about each topic. Only topics documented in the v1 documentation are covered.

This chapter is also useful for developers who are already familiar with Medusa v1 and want to learn about the main differences from Medusa v2. However, it doesn't cover all the new and improved concepts in Medusa v2. Instead, it's highly recommended to read the rest of this documentation to learn about them.

***

## Prerequisites

### Node.js Version

While Medusa v1 supported Node.js v16+, Medusa v2 requires Node.js v20+. So, make sure to update your Node.js version if it's older.

Refer to the [Node.js documentation](https://nodejs.org/en/docs/) for instructions on how to update your Node.js version.

### New Database

Medusa v2 makes big changes to the database. So, your existing database will not be compatible with the database for your v2 project.

If you want to keep your product catalog, you should export the products from the admin dashboard, as explained in [this V1 User Guide](https://docs.medusajs.com/v1/user-guide/products/export/index.html.md). Then, you can import them into your new v2 project from the [Medusa Admin](https://docs.medusajs.com/user-guide/products/import/index.html.md).

For other data types, you'll probably need to migrate them manually through custom scripts. [Custom CLI scripts](https://docs.medusajs.com/learn/fundamentals/custom-cli-scripts/index.html.md) may be useful for this.

***

## How to Upgrade from Medusa v1 to v2

In this section, you'll learn how to upgrade your Medusa project from v1 to v2.

To create a fresh new Medusa v2 project, check out the [Installation chapter](https://docs.medusajs.com/learn/installation/index.html.md).

It's highly recommended to fully go through this chapter before you actually update your application, as some v1 features may have been removed or heavily changed in v2. By doing so, you'll formulate a clearer plan for your migration process and its feasibility.

### 1. Update Dependencies in package.json

The first step is to update the dependencies in your `package.json`.

A basic v2 project has the following dependencies in `package.json`:

```json
{
  "dependencies": {
    "@medusajs/admin-sdk": "2.8.2",
    "@medusajs/cli": "2.8.2",
    "@medusajs/framework": "2.8.2",
    "@medusajs/medusa": "2.8.2"
  },
  "devDependencies": {
    "@medusajs/test-utils": "2.8.2",
    "@swc/core": "1.5.7",
    "@swc/jest": "^0.2.36",
    "@types/jest": "^29.5.13",
    "@types/node": "^20.0.0",
    "@types/react": "^18.3.2",
    "@types/react-dom": "^18.2.25",
    "jest": "^29.7.0",
    "prop-types": "^15.8.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "ts-node": "^10.9.2",
    "typescript": "^5.6.2",
    "vite": "^5.2.11",
    "yalc": "^1.0.0-pre.53"
  }
}
```

The main changes are:

- You need to install the following Medusa packages (All these packages use the same version):
  - `@medusajs/admin-sdk`
  - `@medusajs/cli`
  - `@medusajs/framework`
  - `@medusajs/medusa`
  - `@medusajs/test-utils` (as a dev dependency)
- 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.

Once you're done, run the following command to install the new dependencies:

```bash npm2yarn
npm install
```

In Medusa v1, you needed to install Medusa modules like the Event, Product, or Pricing modules.

These modules are now available out of the box, and you don't need to install or configure them separately.

### 2. Update Script in package.json

Medusa v2 comes with changes and improvements to its CLI tool. So, update your `package.json` with the following scripts:

```json
{
  "scripts": {
    "build": "medusa build",
    "seed": "medusa exec ./src/scripts/seed.ts",
    "start": "medusa start",
    "dev": "medusa develop",
    "test:integration:http": "TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit",
    "test:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit",
    "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit"
  }
}
```

Where:

- `build`: Builds the Medusa application for production.
- `seed`: Seeds the database with initial data.
- `start`: Starts the Medusa server in production.
- `dev`: Starts the Medusa server in development mode.
- `test:integration:http`: Runs HTTP integration tests.
- `test:integration:modules`: Runs module integration tests.
- `test:unit`: Runs unit tests.

You'll learn more about the [changes in the CLI tool later in this chapter](#medusa-cli-changes). You can also refer to the following documents to learn more about these changes:

- [Medusa CLI reference](https://docs.medusajs.com/resources/medusa-cli/index.html.md)
- [Build Medusa Application](https://docs.medusajs.com/learn/build/index.html.md)
- [Integration and Module Tests](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md).

### 3. TSConfig Changes

In Medusa v1, you had multiple TSConfig configuration files for different customization types. For example, you had `tsconfig.admin.json` for admin customizations and `tsconfig.server.json` for server customizations.

In Medusa v2, you only need one [root tsconfig.json](https://github.com/medusajs/medusa-starter-default/blob/master/tsconfig.json) file in your project. For admin customizations, you create a [src/admin/tsconfig.json file](https://github.com/medusajs/medusa-starter-default/blob/master/src/admin/tsconfig.json). Refer to each of those links for the recommended configurations.

### 4. Update Configuration File

In Medusa v1, you configured your application in the `medusa-config.js` file. Medusa v2 supports this file as `medusa-config.ts`, so make sure to rename it.

`medusa-config.ts` now exports configurations created with the `defineConfig` utility. It also uses the [loadEnv](https://docs.medusajs.com/learn/fundamentals/environment-variables/index.html.md) utility to load environment variables based on the current environment.

For example, this is the configuration file for a basic Medusa v2 project:

```ts title="medusa-config.ts"
import { loadEnv, defineConfig } from "@medusajs/framework/utils"

loadEnv(process.env.NODE_ENV || "development", process.cwd())

module.exports = defineConfig({
  projectConfig: {
    databaseUrl: process.env.DATABASE_URL,
    http: {
      storeCors: process.env.STORE_CORS!,
      adminCors: process.env.ADMIN_CORS!,
      authCors: process.env.AUTH_CORS!,
      jwtSecret: process.env.JWT_SECRET || "supersecret",
      cookieSecret: process.env.COOKIE_SECRET || "supersecret",
    },
  },
})
```

You can refer to the full list of configurations in the [Medusa Configurations](https://docs.medusajs.com/learn/configurations/medusa-config/index.html.md) chapter. The following table highlights the main changes between v1 and v2:

|Medusa v1|Medusa v2|
|---|---|
|\`projectConfig.store\_cors\`|projectConfig.http.storeCors|
|\`projectConfig.admin\_cors\`|projectConfig.http.adminCors|
|\`projectConfig.auth\_cors\`|projectConfig.http.authCors|
|\`projectConfig.cookie\_secret\`|projectConfig.http.cookieSecret|
|\`projectConfig.jwt\_secret\`|projectConfig.http.jwtSecret|
|\`projectConfig.database\_database\`|projectConfig.databaseName|
|\`projectConfig.database\_url\`|projectConfig.databaseUrl|
|\`projectConfig.database\_schema\`|projectConfig.databaseSchema|
|\`projectConfig.database\_logging\`|projectConfig.databaseLogging|
|\`projectConfig.database\_extra\`|projectConfig.databaseDriverOptions|
|\`projectConfig.database\_driver\_options\`|projectConfig.databaseDriverOptions|
|\`projectConfig.redis\_url\`|projectConfig.redisUrl|
|\`projectConfig.redis\_prefix\`|projectConfig.redisPrefix|
|\`projectConfig.redis\_options\`|projectConfig.redisOptions|
|\`projectConfig.session\_options\`|projectConfig.sessionOptions|
|\`projectConfig.http\_compression\`|projectConfig.http.compression|
|\`projectConfig.jobs\_batch\_size\`|No longer supported.|
|\`projectConfig.worker\_mode\`|projectConfig.workerMode|
|\`modules\`|Array of modules|

#### Plugin Changes

While the `plugins` configuration hasn't changed, plugins available in Medusa v1 are not compatible with Medusa v2. These are covered later in the [Plugin Changes](#plugin-changes) section.

#### Module Changes

In Medusa v1, you had to configure modules like Inventory, Stock Location, Pricing, and Product. These modules are now available out of the box, and you don't need to install or configure them separately.

For the Cache and Event modules, refer to the [Redis Caching Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/caching/providers/redis/index.html.md) and [Redis Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md) documentations to learn how to configure them in v2 if you had them configured in v1.

#### Feature Flags

Some features like product categories and tax inclusive pricing were disabled behind feature flags.

All of these features are now available out-of-the-box. So, you don't need to enable them in your configuration file anymore.

#### Admin Configurations

In v1, the admin dashboard was installed as a plugin with configurations. In v2, the Medusa Admin is available out-of-the-box with different configurations.

The [Medusa Admin Changes](#medusa-admin-changes) section covers the changes in the Medusa Admin configurations and customizations.

### 5. Setup New Database

Now that you have updated your dependencies and configuration file, you need to set up the database for your v2 project.

This will not take into account entities and data customizations in your v1 project, as you still need to change those. Instead, it will only create the database and tables for your v2 project.

First, change your database environment variables to the following:

```bash
DATABASE_URL=postgres://localhost/$DB_NAME
DB_NAME=medusa-v2
```

You can change `medusa-v2` to any database name you prefer.

Then, run the following commands to create the database and tables:

```bash npm2yarn
npx medusa db:setup
```

This command will create the database and tables for your v2 project.

After that, you can start your Medusa application with the `dev` command. Note that you may have errors if you need to make implementation changes that are covered in the rest of this guide, so it's better to wait until you finish the v2 migration process before starting the Medusa application.

### (Optional) 6. Seed with Demo Data

If you want to seed your Medusa v2 project with demo data, you can copy the content of [this file](https://github.com/medusajs/medusa-starter-default/blob/master/src/scripts/seed.ts) to your `src/scripts/seed.ts` file.

Then, run the following command to seed the database:

```bash npm2yarn
npm run seed
```

This will seed your database with demo data.

***

## Medusa Admin Changes

In this section, you'll learn about the changes in the Medusa Admin between v1 and v2.

This section doesn't cover changes related to Medusa Admin customizations. They're covered later in the [Admin Customization Changes](#admin-customization-changes) section.

The Medusa Admin is now available out-of-the-box. It's built with [Vite v5](https://vite.dev/) and runs at `http://localhost:9000/app` by default when you start your Medusa application.

### Admin Configurations

You previously configured the admin dashboard when you added it as a plugin in Medusa v1.

In Medusa v2, you configure the Medusa Admin within the `defineConfig` utility in `medusa-config.ts`. `defineConfig` accepts an `admin` property to configure the Medusa Admin:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  admin: {
    // admin options...
  },
})
```

You can refer to the [Medusa Configuration](https://docs.medusajs.com/learn/configurations/medusa-config#admin-configurations-admin/index.html.md) chapter to learn about all the admin configurations. The following table highlights the main changes between v1 and v2:

|Medusa v1|Medusa v2|
|---|---|
|\`serve\`|admin.disable|
|\`autoRebuild\`|No longer supported. The Medusa Admin is always built when you run the |
|\`backend\`|admin.backendUrl|
|\`outDir\`|No longer supported. The Medusa Admin is now built in the |
|\`develop\`|No longer supported. The |

### Admin Webpack Configurations

In v1, you were able to modify Webpack configurations of the admin dashboard.

Since Medusa Admin is now built with Vite, you can modify the Vite configurations with the `admin.vite` configuration. Learn more in the [Medusa Configuration](https://docs.medusajs.com/learn/configurations/medusa-config#vite/index.html.md) chapter.

### Admin CLI Tool

In Medusa v1, you used the `medusa-admin` CLI tool to build and run the admin dashboard.

In Medusa v2, the Medusa Admin doesn't have a CLI tool. Instead, running `medusa build` and `medusa develop` also builds and runs the Medusa Admin, respectively.

In addition, you can build the Medusa Admin separately from the Medusa application using the `--admin-only` option. Learn more in the [Build Medusa Application](https://docs.medusajs.com/learn/build#separate-admin-build/index.html.md) chapter.

***

## Medusa CLI Changes

The Medusa CLI for v2 is now in the `@medusajs/cli` package. However, you don't need to install it globally. You can just use `npx medusa` in your Medusa projects.

Refer to the [Medusa CLI reference](https://docs.medusajs.com/resources/medusa-cli/index.html.md) for the full list of commands and options. The following table highlights the main changes between v1 and v2:

|Medusa v1|Medusa v2|
|---|---|
|\`migrations run\`|db:migrate|
|\`migrations revert\`|db:rollback|
|\`migrations show\`|No longer supported.|
|\`seed\`|No longer supported. However, you can create a |
|\`start-cluster\`|start --cluster \<number>|

***

## Plugin Changes

Medusa v2 supports plugins similar to Medusa v1, but with changes in its usage, development, and configuration.

In Medusa v1, you created plugins that contained customizations like services that integrated third-party providers, custom API routes, and more.

In Medusa v2, a plugin can contain customizations like modules that integrate third-party providers, custom API routes, workflows, and more. The plugin development experience has also been improved to resolve big pain points that developers faced in v1.

Refer to the [Plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) chapter to learn more about plugins in Medusa v2.

The rest of this section will cover some of the main changes in plugins between v1 and v2.

### Medusa Plugins Alternative

In v1, Medusa provided a set of plugins that you could use in your project. For example, the Stripe and SendGrid plugins.

In v2, some of these plugins are now available as module providers out-of-the-box. For example, the Stripe and SendGrid module providers. Other plugins may no longer be available, but you can still find guides to create them.

The following table highlights the alternatives for the Medusa v1 plugins:

|Medusa v1|v2 Alternative|
|---|---|
|Algolia|Guide|
|Brightpearl|Not available, but you can follow the |
|Contenful|Guide|
|Discount Generator|Not available, but you can build it |
|IP Lookup|Not available, but you can build it |
|Klarna|Not available, but you can integrate it as a |
|Local File|Local File Module Provider|
|Mailchimp|Guide|
|MinIO|S3 (compatible APIs) File Module Provider|
|MeiliSearch|Not available, but you can integrate it |
|PayPal|Not available, but you can integrate it as a |
|Restock Notification|Guide|
|S3|S3 (compatible APIs) File Module Provider|
|Segment|Guide|
|SendGrid|SendGrid Module Provider|
|Shopify|Not available, but you can build it |
|Slack|Guide|
|Spaces (DigitalOcean)|S3 (compatible APIs) File Module Provider|
|Strapi|Not available, but you can integrate it |
|Stripe|Stripe Payment Module Provider|
|Twilio|Guide|
|Wishlist|Guide|

You can also find Medusa and community integrations in the [Integrations](https://medusajs.com/integrations/) page.

### Plugin Options

Similar to Medusa v1, you can pass options to plugins in Medusa v2.

However, plugin options are now only passed to modules and module providers created in a plugin.

So, if you previously accessed options in a plugin's subscriber, for example, that's not possible anymore. You need to access the options in a module or module provider instead, then use its service in the plugin's subscriber.

For example, this is how you can access options in a plugin's subscriber in v2:

```ts title="src/subscribers/order-placed.ts"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"

export default async function orderPlacedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const customModuleService = container.resolve("custom")

  const options = customModuleService.getOptions()

  // Use the options in your logic...
}

export const config: SubscriberConfig = {
  event: `order.placed`,
}
```

Learn more in the [Create Plugin](https://docs.medusajs.com/learn/fundamentals/plugins/create/index.html.md) chapter.

#### enableUI Option

Plugins in v1 accepted an `enableUI` option to configure whether a plugin's admin customizations should be shown.

In v2, this option is no longer supported. All admin customizations in a plugin will be shown in the Medusa Admin.

***

## Tool Changes

This section covers changes to tools that were available in Medusa v1.

|Medusa v1|Medusa v2|
|---|---|
|JS Client|JS SDK|
|Medusa React|No longer supported. Instead, you can |
|Next.js Starter Template|Next.js Starter Storefront|
|Medusa Dev CLI|No longer supported.|

***

## Changes in Concepts and Development

In the next sections, you'll learn about the changes in specific concepts and development practices between Medusa v1 and v2.

### Entities, Services, and Modules

In Medusa v1, entities, services, and modules were created separately:

- You create an entity to add a new table to the database.
- You create a service to add new business logic to the Medusa application.
- You create a module to add new features to the Medusa application. It may include entities and services.

In Medusa v2, you create entities (now called data models) and services in a module. You can't create them separately anymore. The data models define new tables to add to the database, and the service provides data-management features for those data models.

In this section, you'll learn about the most important changes related to these concepts. You can also learn more in the [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) chapter.

#### Modules

A module is a reusable package of functionalities related to a single domain or integration. For example, Medusa provides a Product Module for product-related data models and features.

So, if in Medusa v1 you had a `Brand` entity and a service to manage it, in v2, you create a Brand Module that defines a `Brand` data model and a service to manage it.

To learn how to create a module, refer to the [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) chapter.

![Diagram showcasing the directory structure difference between Medusa v1 and v2](https://res.cloudinary.com/dza7lstvk/image/upload/v1748277500/Medusa%20Book/modules-v1-v2_dsnzyl.jpg)

#### Data Models

In Medusa v1, you created data models (entities) using TypeORM.

In Medusa v2, you use Medusa's Data Model Language (DML) to create data models. It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

For example:

```ts title="src/modules/brand/models/brand.ts"
import { model } from "@medusajs/framework/utils"

export const Brand = model.define("brand", {
  id: model.id().primaryKey(),
  name: model.text(),
})
```

Learn more about data models in the [Data Models](https://docs.medusajs.com/learn/fundamentals/data-models/index.html.md) chapters.

#### Migrations

In Medusa v1, you had to write migrations manually to create or update tables in the database. Migrations were based on TypeORM.

In Medusa v2, you can use the Medusa CLI to generate migrations based on MikroORM. For example:

```bash npm2yarn
npx medusa db:generate brand
```

This generates migrations for data models in the Brand Module. Learn more in the [Migrations](https://docs.medusajs.com/learn/fundamentals/data-models/write-migration/index.html.md) chapter.

#### Services

In Medusa v1, you created a service with business logic related to a feature within your Medusa project. For example, you created a `BrandService` at `src/services/brand.ts` to manage the `Brand` entity.

In Medusa v2, you can only create a service in a module, and the service either manages the module's data models in the database, or connects to third-party services.

For example, you create a `BrandService` in the Brand Module at `src/modules/brand/service.ts`:

```ts title="src/modules/brand/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import { Brand } from "./models/brand"

class BrandModuleService extends MedusaService({
  Brand,
}) {

}

export default BrandModuleService
```

The service has automatically generated data-management methods by extending `MedusaService` from the Modules SDK. So, you now have methods like `retrieveBrand` and `createBrands` available in the service.

Learn more in the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) chapter.

When you register the module in the Medusa application, the service is registered in the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md), allowing you to use its methods in workflows, subscribers, scheduled jobs, and API routes.

#### Repositories

In Medusa v1, you used the repository of a data model in a service to provide data-management features. For example, you used the `BrandRepository` to manage the `Brand` entity. Repositories were also based on TypeORM.

In Medusa v2, you generally don't need repositories for basic data-management features, as they're generated by the service factory. However, for more complex use cases, you can use the data model repository based on MikroORM.

For example:

```ts title="src/modules/brand/service.ts"
import { InferTypeOf, DAL } from "@medusajs/framework/types"
import Post from "./models/post"

type Post = InferTypeOf<typeof Post>

type InjectedDependencies = {
  postRepository: DAL.RepositoryService<Post>
}

class BlogModuleService {
  protected postRepository_: DAL.RepositoryService<Post>

  constructor({ 
    postRepository, 
  }: InjectedDependencies) {
    super(...arguments)
    this.postRepository_ = postRepository
  }
}

export default BlogModuleService
```

Learn more in the [Database Operations](https://docs.medusajs.com/learn/fundamentals/modules/db-operations/index.html.md) chapter.

#### Module Isolation

In Medusa v1, you had access to all entities and services in the Medusa application. While this approach was flexible, it introduced complexities, was difficult to maintain, and resulted in hacky workarounds.

In Medusa v2, modules are isolated. This means that you can only access entities and services within the module. This isolation allows you to integrate modules into your application without side effects, while still providing you with the necessary flexibility to build your use cases.

The [Module Isolation](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md) chapter explains this concept in detail. The rest of this section gives a general overview of how module isolation affects your Medusa v1 customizations.

#### Extending Entities

In Medusa v1, you were able to extend entities by creating a new entity that extended the original one. For example, you could create a custom `Product` entity that extended the original `Product` entity to add a `brand` column.

In Medusa v2, you can no longer extend entities. Instead, you need to create a new data model that contains the columns you want to add. Then, you can create a [Module Link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) that links your data model to the one you want to extend.

For example, you create a Brand Module that has a `Brand` data model. Then, you create a Module Link that links the `Brand` data model to the `Product` data model in the Product Module:

```ts title="src/links/product-brand.ts"
import BrandModule from "../modules/brand"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    isList: true,
  },
  BrandModule.linkable.brand
)
```

You can then associate brands with a product, retrieve them in API routes and custom functionalities, and more.

Learn more in the [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) chapter.

#### Extending Services

In Medusa v1, you were able to extend services by creating a new service that extended the original one. For example, you could create a custom `ProductService` that extended the original `ProductService` to add a new method.

In Medusa v2, you can no longer extend services. Instead, you need to [create a module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) with a service that contains the methods you want to add. Then, you can:

- Build [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that use both services to achieve a custom feature.
- Consume [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) to run custom actions in existing workflows.
- For more complex use cases, you can re-create an existing workflow and use your custom module's service in it.

For example, if you extended the `CartService` in v1 to add items with custom prices to the cart, you can instead build a custom workflow that uses your custom module to retrieve an item's price, then add it to the cart using the existing `addToCartWorkflow`:

```ts
import { 
  createWorkflow,
  transform,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { addToCartWorkflow } from "@medusajs/medusa/core-flows"
import { 
  getCustomPriceStep, 
} from "./steps/get-custom-price"

type AddCustomToCartWorkflowInput = {
  cart_id: string
  item: {
    variant_id: string
    quantity: number
    metadata?: Record<string, unknown>
  }
}

export const addCustomToCartWorkflow = createWorkflow(
  "add-custom-to-cart",
  ({ cart_id, item }: AddCustomToCartWorkflowInput) => {
    // assuming this step uses a custom module to get the price
    const price = getCustomPriceStep({
      variant: item.variant_id,
      currencyCode: "usd",
      quantity: item.quantity,
    })

    const itemToAdd = transform({
      item,
      price,
    }, (data) => {
      return [{
        ...data.item,
        unit_price: data.price,
      }]
    })

    addToCartWorkflow.runAsStep({
      input: {
        items: itemToAdd,
        cart_id,
      },
    })
  }
)
```

Refer to the [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) chapters to learn more about workflows in Medusa v2.

#### Integrating Third-Party Services

In Medusa v1, you integrated third-party services by creating a service under `src/services` and using it in your customizations.

In Medusa v2, you can integrate third-party services by creating a module with a service that contains the methods to interact with the third-party service. You can then use the module's service in a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) to build custom features.

![Directory structure change between v1 and v2](https://res.cloudinary.com/dza7lstvk/image/upload/v1748278103/Medusa%20Book/integrations-v1-v2_cjzkus.jpg)

***

### Medusa and Module Containers

In Medusa v1, you accessed dependencies from the container in all your customizations, such as services, API routes, and subscribers.

In Medusa v2, there are two containers:

|Container|Description|Accessed By|
|---|---|---|
|Medusa container|Main container that contains Framework and commerce resources, such as services of registered modules.||
|Module container|Container of a module. It contains some resources from the Framework, and resources implemented in the module.|Services and loaders in the module.|

You can view the list of resources in each container in the [Container Resources](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md) reference.

***

### Workflow Changes

In Medusa v2, workflows are the main way to implement custom features spanning across modules and systems.

Workflows have been optimized for data reliability, flexibility, and orchestration across systems. You can learn more in the [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) chapters.

This section highlights the main changes in workflows between v1 and v2.

#### Workflows SDK Imports

In Medusa v1, you imported all Workflows SDK functions and types from the `@medusajs/workflows-sdk` package.

In Medusa v2, you import them from the `@medusajs/framework/workflows-sdk` package. For example:

```ts title="src/workflows/hello-world.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
```

#### Workflow Return Value

In Medusa v1, you returned any value from a workflow, such as a string or an object.

In Medusa v2, you must return an instance of `WorkflowResponse` from a workflow. The data passed to `WorkflowResponse`'s constructor is returned to the caller of the workflow.

For example:

```ts title="src/workflows/hello-world.ts"
import { 
  createWorkflow, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

export const helloWorldWorkflow = createWorkflow(
  "hello-world", 
  () => {
    return new WorkflowResponse("Hello, world!")
  }
)

// in API route, for example:
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  // message is "Hello, world!"
  const { result: message } = await helloWorldWorkflow(req.scope)
    .run()

  res.json({
    message,
  })
}
```

#### New Workflow Features

- [Use when-then in workflows to run steps if a condition is satisfied](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md).
- [Consume hooks to run custom steps in existing workflows](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md).
- [Create long-running workflows that run asynchronously in the background](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md).

***

### API Route Changes

API routes are generally similar in Medusa v1 and v2, but with minor changes.

You can learn more about creating API routes in the [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) chapters. This section highlights the main changes in API routes between v1 and v2.

#### HTTP Imports

In Medusa v1, you imported API-route related types and functions from the `@medusajs/medusa` package.

In Medusa v2, you import them from the `@medusajs/framework/http` package. For example:

```ts title="src/api/store/custom/route.ts"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
```

#### Protected API Routes

In Medusa v1, routes starting with `/store/me` and `/admin` were protected by default.

In Medusa v2, routes starting with `/store/customers/me` are accessible by registered customers, and `/admin` routes are accessible by admin users.

In an API route, you can access the logged in user or customer using the `auth_context.actor_id` property of `AuthenticatedMedusaRequest`. For example:

```ts title="src/api/store/custom/route.ts"
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const id = req.auth_context?.actor_id

  // ...
}
```

Learn more in the [Protected API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/protected-routes/index.html.md) chapter.

#### Authentication Middlewares

In Medusa v1, you had three middlewares to protect API routes:

- `authenticate` to protect API routes for admin users.
- `authenticateCustomer` to optionally authenticate customers.
- `requireCustomerAuthentication` to require customer authentication.

In Medusa v2, you can use a single `authenticate` middleware for the three use cases. For example:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  authenticate,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom/admin*",
      middlewares: [authenticate("user", ["session", "bearer", "api-key"])],
    },
    {
      matcher: "/custom/customer*",
      // equivalent to requireCustomerAuthentication
      middlewares: [authenticate("customer", ["session", "bearer"])],
    },
    {
      matcher: "/custom/all-customers*",
      // equivalent to authenticateCustomer
      middlewares: [authenticate("customer", ["session", "bearer"], {
        allowUnauthenticated: true,
      })],
    },
  ],
})
```

Learn more in the [Protected API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/protected-routes/index.html.md) chapter.

#### Middlewares

In Medusa v1, you created middlewares by exporting an object in the `src/api/middlewares.ts` file.

In Medusa v2, you create middlewares by exporting an object created with `defineMiddlewares`, which accepts an object with the same properties as in v1. For example:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse, 
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      middlewares: [
        (
          req: MedusaRequest, 
          res: MedusaResponse, 
          next: MedusaNextFunction
        ) => {
          console.log("Received a request!")

          next()
        },
      ],
    },
  ],
})
```

Learn more in the [Middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) chapter.

#### Disable Body Parser

In Medusa v1, you disabled the body parser in API routes by setting `bodyParser: false` in the route's middleware configuration.

In Medusa v2, you disable the body parser by setting `bodyParser.preserveRawBody` to `true` in the route's middleware configuration. For example:

```ts title="src/api/middlewares.ts"
import { defineMiddlewares } from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      method: ["POST"],
      bodyParser: { preserveRawBody: true },
      matcher: "/custom",
    },
  ],
})
```

Learn more in the [Body Parser](https://docs.medusajs.com/learn/fundamentals/api-routes/parse-body/index.html.md) chapter.

#### Extending Validators

In Medusa v1, you passed custom request parameters to Medusa's API routes by extending a request's validator.

In Medusa v2, some Medusa API routes support passing additional data in the request body. You can then configure the validation of that additional data and consume them in the hooks of the workflow used in the API route.

For example:

```ts title="src/api/middlewares.ts"
import { defineMiddlewares } from "@medusajs/framework/http"
import { z } from "zod"

export default defineMiddlewares({
  routes: [
    {
      method: "POST",
      matcher: "/admin/products",
      additionalDataValidator: {
        brand: z.string().optional(),
      },
    },
  ],
})
```

In this example, you allow passing a `brand` property as additional data to the `/admin/products` API route.

You can learn more in the [Additional Data](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md) chapter.

If a route doesn't support passing additional data, you need to [replicate it](https://docs.medusajs.com/learn/fundamentals/api-routes/override/index.html.md) to support your custom use case.

***

### Events and Subscribers Changes

Events and subscribers are similar in Medusa v1 and v2, but with minor changes.

You can learn more in the [Events and Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) chapters. This section highlights the main changes in events and subscribers between v1 and v2.

#### Emitted Events

Medusa v2 doesn't emit the same events as v1. Refer to the [Events Reference](https://docs.medusajs.com/resources/references/events/index.html.md) for the full list of events emitted in v2.

#### Subscriber Type Imports

In Medusa v1, you imported subscriber types from the `@medusajs/medusa` package.

In Medusa v2, you import them from the `@medusajs/framework` package. For example:

```ts title="src/subscribers/order-placed.ts"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
```

#### Subscriber Parameter Change

In Medusa v1, a subscriber function received an object parameter that has `eventName` and `data` properties.

In Medusa v2, the subscriber function receives an object parameter that has an `event` property. The `event` property contains the event name and data. For example:

```ts title="src/subscribers/order-placed.ts"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"

export default async function orderPlacedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  // ...
}

export const config: SubscriberConfig = {
  event: `order.placed`,
}
```

Also, the `pluginOptions` property is no longer passed in the subscriber's parameter. Instead, you can access the options passed to a plugin within its modules' services, which you can resolve in a subscriber.

Learn more in the [Events and Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) chapter.

#### Subscriber Implementation Change

In Medusa v1, you implemented functionalities, such as sending confirmation email, directly within a subscriber.

In Medusa v2, you should implement these functionalities in a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) and call the workflow in the subscriber. By using workflows, you benefit from rollback mechanism, among other features.

For example:

```ts title="src/subscribers/order-placed.ts"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { 
  sendOrderConfirmationWorkflow,
} from "../workflows/send-order-confirmation"

export default async function orderPlacedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await sendOrderConfirmationWorkflow(container)
    .run({
      input: {
        id: data.id,
      },
    })
}

export const config: SubscriberConfig = {
  event: `order.placed`,
}
```

#### Emitting Events

In Medusa v1, you emitted events in services and API routes by resolving the Event Module's service from the container.

In Medusa v2, you should emit events in workflows instead. For example:

```ts title="src/workflows/hello-world.ts"
import { 
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import {
  emitEventStep,
} from "@medusajs/medusa/core-flows"

const helloWorldWorkflow = createWorkflow(
  "hello-world",
  () => {
    // ...

    emitEventStep({
      eventName: "custom.created",
      data: {
        id: "123",
        // other data payload
      },
    })
  }
)
```

If you need to emit events in a service, you can add the Event Module as a dependency of your module. Then, you can resolve the Event Module's service from the module container and emit the event. This approach is only recommended for events related to under-the-hood processes.

Learn more in the [Emit Events](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/emit-event/index.html.md) chapter.

***

### Loader Changes

In Medusa v1, you created loaders in the `src/loaders` directory to perform tasks at application startup.

In Medusa v2, loaders can only be created in a module. You can create loaders in the `src/modules/<module-name>/loaders` directory. That also means the loader can only access resources in the module's container.

Learn more in the [Loaders](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md) chapter.

#### Loader Parameter Changes

In Medusa v1, a loader function receives two parameters: `container` and `config`. If the loader was created in a module, it also received a `logger` parameter.

In Medusa v2, a loader function receives a single object parameter that has a `container` and `options` properties. The `options` property contains the properties passed to the module.

For example:

```ts title="src/modules/hello/loaders/hello-world.ts"
import {
  LoaderOptions,
} from "@medusajs/framework/types"

export default async function helloWorldLoader({
  container,
  options,
}: LoaderOptions) {
  const logger = container.resolve("logger")

  logger.info("[HELLO MODULE] Just started the Medusa application!")
}
```

***

### Scheduled Job Changes

Scheduled jobs are similar in Medusa v1 and v2, but with minor changes.

You can learn more about scheduled jobs in the [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) chapters. This section highlights the main changes in scheduled jobs between v1 and v2.

#### Scheduled Job Parameter Changes

In Medusa v1, a scheduled job function received an object of parameters.

In Medusa v2, a scheduled job function receives only the Medusa container as a parameter. For example:

```ts title="src/jobs/hello-world.ts"
import { MedusaContainer } from "@medusajs/framework/types"

export default async function greetingJob(container: MedusaContainer) {
  const logger = container.resolve("logger")

  logger.info("Greeting!")
}

export const config = {
  name: "greeting-every-minute",
  schedule: "* * * * *",
}
```

The `pluginOptions` property is no longer available, as you can access the options passed to a plugin within its modules' services, which you can resolve in a scheduled job.

The `data` property is also no longer available, as you can't pass data in the scheduled job's configuration anymore.

#### Scheduled Job Configuration Changes

In Medusa v2, the `data` property is removed from the scheduled job's configuration object.

#### Scheduled Job Implementation Changes

In Medusa v1, you implemented functionalities directly in the job function.

In Medusa v2, you should implement these functionalities in a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) and call the workflow in the scheduled job. By using workflows, you benefit from rollback mechanism, among other features.

For example:

```ts title="src/jobs/sync-products.ts"
import { MedusaContainer } from "@medusajs/framework/types"
import { syncProductToErpWorkflow } from "../workflows/sync-products-to-erp"

export default async function syncProductsJob(container: MedusaContainer) {
  await syncProductToErpWorkflow(container)
    .run()
}

export const config = {
  name: "sync-products-job",
  schedule: "0 0 * * *",
}
```

***

### Removed Concepts and Alternatives

The following table highlights concepts that have been removed or changed in Medusa v2 and their alternatives:

|Medusa v1|Medusa v2|
|---|---|
|Batch Jobs and Strategies|Long-Running Workflows|
|File Service|File Module Provider|
|Notification Provider Service|Notification Module Provider|
|Search Service|Can be integrated as a |

***

### Admin Customization Changes

This section covers changes to the admin customizations between Medusa v1 and v2.

#### Custom Admin Environment Variables

In Medusa v1, you set custom environment variables to be passed to the admin dashboard by prefixing them with `MEDUSA_ADMIN_`.

In Medusa v2, you can set custom environment variables to be passed to the admin dashboard by prefixing them with `VITE_`. Learn more in the [Admin Environment Variables](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md) chapter.

#### Admin Widgets

Due to design changes in the Medusa Admin, some widget injection zones may have been changed or removed. Refer to the [Admin Widgets Injection Zones](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md) reference for the full list of injection zones in v2.

Also, In Medusa v1, you exported in the widget's file a `config` object with the widget's configurations, such as its injection zone.

In Medusa v2, you export a configuration object defined with `defineWidgetConfig` from the Admin Extension SDK. For example:

```tsx title="src/admin/widgets/product-widget.tsx" highlights={[["9"], ["10"], ["11"]]}
import { defineWidgetConfig } from "@medusajs/admin-sdk"

// The widget
const ProductWidget = () => {
  // ...
}

// The widget's configurations
export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

The function accepts an object with a `zone` property, indicating the zone to inject the widget.

Refer to the [Admin Widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md) chapter to learn more about creating widgets in Medusa v2.

### Admin UI Routes

In Medusa v1, UI Routes were prefixed by `/a`. For example, a route created at `src/admin/routes/custom/page.tsx` would be available at `http://localhost:9000/a/custom`.

In Medusa v2, the `/a` prefix has been removed. So, that same route would be available at `http://localhost:9000/app/custom` (where `/app` is the path to the admin dashboard, not a prefix).

Also, in v1, you exported a `config` object with the route's configurations to show the route in the dashboard's sidebar.

In v2, you export a configuration object defined with `defineRouteConfig` from the Admin Extension SDK. For example:

```tsx title="src/admin/routes/custom/page.tsx" highlights={[["8"], ["9"], ["10"], ["11"]]}
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"

const CustomPage = () => {
  // ...
}

export const config = defineRouteConfig({
  label: "Custom Route",
  icon: ChatBubbleLeftRight,
})

export default CustomPage
```

The `defineRouteConfig` function accepts an object with the following properties:

- `label`: The label of the route to show in the sidebar.
- `icon`: The icon to use in the sidebar for the route.

Refer to the [Admin UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md) chapter to learn more about creating UI routes in Medusa v2.

### Admin Setting Routes

In Medusa v1, you created setting pages under the `src/admin/settings` directory with their own configurations.

In Medusa v2, setting pages are UI routes created under the `src/admin/routes/settings` directory.

For example, if you had a `src/admin/settings/custom/page.tsx` file in v1, you should move it to `src/admin/routes/settings/custom/page.tsx` in v2. The file's content will be the same as a UI route.

For example:

```tsx title="src/admin/routes/settings/custom/page.tsx" highlights={[["14"], ["15"], ["16"]]}
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

const CustomSettingPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h1">Custom Setting Page</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Custom",
})

export default CustomSettingPage
```

In v1, you exported a `config` object that showed a setting page as a card in the settings page. In v2, you export the same configuration object as a UI route.

Learn more about creating setting pages in the [Admin UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes#create-settings-page/index.html.md) chapter.

### notify Props in Widgets, UI Routes, and Settings

In Medusa v1, admin widgets, UI routes, and setting pages received a `notify` prop to show notifications in the admin dashboard.

This prop is no longer passed in v2. Instead, use the [toast utility from Medusa UI](https://docs.medusajs.com/ui/components/toast/index.html.md) to show notifications.

For example:

```tsx title="src/admin/widgets/product-details.tsx" highlights={[["7"], ["8"], ["9"]]}
import { toast } from "@medusajs/ui"
import { defineWidgetConfig } from "@medusajs/admin-sdk"

// The widget
const ProductWidget = () => {
  const handleOnClick = () => {
    toast.info("Info", {
      description: "The quick brown fox jumps over the lazy dog.",
    })
  }
  // ...
}

// The widget's configurations
export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

Learn more about the `toast` utility in the [Medusa UI Toasts](https://docs.medusajs.com/ui/components/toast/index.html.md) documentation.

### Sending Requests to Medusa Server

In Medusa v1, you used Medusa React to send requests to the Medusa server.

Medusa v2 no longer supports Medusa React. Instead, you can use the JS SDK with Tanstack Query to send requests from your admin customizations to the Medusa server.

Learn more in the [Admin Development Tips](https://docs.medusajs.com/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md) chapter.

### Admin Languages

Medusa Admin v2 supports different languages out-of-the-box, and you can contribute with new translations.

Refer to the [User Guide](https://docs.medusajs.com/user-guide/tips/languages/index.html.md) for the list of languages supported in the Medusa Admin.

***

## Commerce Features Changes

In Medusa v2, commerce features are implemented as [Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md). For example, the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) implements the product-related features, whereas the [Cart Module](https://docs.medusajs.com/resources/commerce-modules/cart/index.html.md) implements the cart-related features.

So, it's difficult to cover all changes in commerce features between v1 and v2. Instead, this section will highlight changes to customizations that were documented in the Medusa v1 documentation.

To learn about all commerce features in Medusa v2, refer to the [Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md) documentation.

### Providers are now Module Providers

In Medusa v1, you created providers for payment, fulfillment, and tax in services under `src/services`.

In Medusa v2, you create these providers as module providers that belong to the Payment, Fulfillment, and Tax modules respectively.

Refer to the following guides to learn how to create these module providers:

- [Payment Module Provider](https://docs.medusajs.com/resources/commerce-modules/payment/payment-provider/index.html.md)
- [Fulfillment Module Provider](https://docs.medusajs.com/resources/commerce-modules/fulfillment/fulfillment-provider/index.html.md)
- [Tax Module Provider](https://docs.medusajs.com/resources/commerce-modules/tax/tax-provider/index.html.md)

### Overridden Cart Completion

In Medusa v1, you were able to override the cart completion strategy to customize the cart completion process.

In Medusa v2, the cart completion process is now implemented in the [completeCartWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/completeCartWorkflow/index.html.md). There are two ways you can customize the completion process:

- [Consuming hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) like the [validate](https://docs.medusajs.com/resources/references/medusa-workflows/completeCartWorkflow#validate/index.html.md) hook. This is useful if you only want to make changes in key points of the cart completion process.
  - You can view available hooks in the [completeCartWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/completeCartWorkflow#hooks/index.html.md)
- For more complex use cases, you can create a new workflow with the desired functionality. Then, you can [replicate the complete cart API route](https://docs.medusajs.com/learn/fundamentals/api-routes/override/index.html.md) and use it in your storefront.

### Overridden Tax Calculation

In Medusa v1, you were able to override the tax calculation strategy to customize the tax calculation process.

In Medusa v2, the tax calculation process is now implemented in a [Tax Module Provider](https://docs.medusajs.com/resources/commerce-modules/tax/tax-provider/index.html.md). So, you can [create a custom tax provider](https://docs.medusajs.com/resources/references/tax/provider/index.html.md) with the calculation logic you want, then [use it in a tax region](https://docs.medusajs.com/user-guide/settings/tax-regions#edit-tax-region/index.html.md).

### Overridden Price Selection

In Medusa v1, you were able to override the price selection strategy to customize the price selection process.

In Medusa v2, the price selection process is now implemented in the [Pricing Module's calculate method](https://docs.medusajs.com/resources/commerce-modules/pricing/price-calculation/index.html.md). The Pricing Module allows you to set [flexible rules and tiers](https://docs.medusajs.com/resources/commerce-modules/pricing/price-rules/index.html.md) to support your use case.

If your use case is complex and these rules are not enough, you can create a new [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) with the necessary logic, then use that module in your custom workflows.

## Prices are Stored in Major Units

In Medusa v1, prices were stored in the smallest currency unit. For example, a price of $10.00 was stored as `1000` (cents).

In Medusa v2, prices are stored in the major unit. For example, a price of $10.00 is stored as `10` (dollars).

Learn more in the [Pricing Concepts](https://docs.medusajs.com/resources/commerce-modules/pricing/concepts/index.html.md) guide.

### Gift Card Features

Medusa v1 has gift card features out-of-the-box.

In Medusa v2, gift card features are now only available to [Cloud](https://medusajs.com/cloud/) users.

***

## Deployment Changes

The deployment process in Medusa v2 is similar to v1, but with some changes. For example, the Medusa server is now deployed with Medusa Admin.

Medusa also provides [Cloud](https://medusajs.com/cloud/), a managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure.

Refer to the [Deployment](https://docs.medusajs.com/resources/deployment/index.html.md) documentation to learn about the deployment process for Medusa applications and Next.js Starter Storefront.


# Introduction

Medusa is a digital commerce platform with a built-in Framework for customization.

Medusa ships with three main tools:

1. A suite of [Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md) with core commerce functionalities, such as tracking inventory, calculating cart totals, accepting payments, managing orders, and much more.
2. A [Framework](https://docs.medusajs.com/learn/fundamentals/framework/index.html.md) for building custom functionalities specific to your business, product, or industry. This includes tools for introducing custom API endpoints, business logic, and data models; building workflows and automations; and integrating with third-party services.
3. A customizable admin dashboard for merchants to configure and operate their store.

When you install Medusa, you get a fully fledged commerce platform with all the features you need to get off the ground. However, unlike other platforms, Medusa is built with customization in mind. You don't need to build hacky workarounds that are difficult to maintain and scale. Your efforts go into building features that bring your business's vision to life.

***

## Who should use Medusa

Medusa is for businesses and teams looking for a digital commerce platform with the tools to implement unique requirements that other platforms aren't built to support.

Businesses of all sizes can use Medusa, from small start ups to large enterprises. Also, technical teams of all sizes can build with Medusa; all it takes is a developer to manage and deploy Medusa projects.

Below are some stories from companies that use Medusa:

- [Use Case: Advanced Fulfillment](https://medusajs.com/blog/eight-sleep/): How Eight Sleeps built their fulfillment setup with Medusa
- [Use Case: D2C](https://medusajs.com/blog/matt-sleeps/): How Matt Sleeps built a unique D2C experience with Medusa
- [Use Case: Marketplace](https://medusajs.com/blog/foraged/): How Foraged built a custom marketplace with Medusa
- [Use Case: Distributor Platform](https://medusajs.com/blog/redington): How Redington built a B2B distributor platform with Medusa
- [Use Case: Quick to market](https://medusajs.com/blog/partbase): How Partbase built their distributor platform with Medusa
- [Use Case: Complex products & pricing](https://medusajs.com/blog/eki/): How EKI built a B2B platform with complex products and pricing

***

## Who is this documentation for

This documentation introduces you to Medusa's concepts and how they help you build your business use case. The documentation is structured to gradually introduce Medusa's concepts, with easy-to-follow examples along the way.

By following this documentation, you'll be able to create custom commerce experiences that would otherwise take large engineering teams months to build.

### How to use the documentation

This documentation is split into the following sections:

|Section|Description|
|---|---|---|
|Main Documentation|The documentation you're currently reading. It's recommended to follow the chapters in this documentation to understand the core concepts of Medusa and how to use them before jumping into the other sections.|
|Product|Documentation for the |
|Build|Recipes|
|Tools|Guides on how to setup and use Medusa's CLI tools, |
|API Routes References|References of the |
|References|Useful during your development with Medusa to learn about different APIs and how to use them. Its references include the |
|User Guide|Guides that introduce merchants and store managers to the Medusa Admin dashboard and helps them understand how to use the dashboard to manage their store.|
|Cloud|Learn about Cloud, our managed services offering for Medusa applications. Find guides on how to deploy your Medusa application, manage organizations, and more.|

To get started, check out the [Installation chapter](https://docs.medusajs.com/learn/installation/index.html.md).

***

## Useful Links

- Need Help? Refer to our [GitHub repository](https://github.com/medusajs/medusa) for [issues](https://github.com/medusajs/medusa/issues) and [discussions](https://github.com/medusajs/medusa/discussions).
- [Join the community on Discord](https://discord.gg/medusajs).
- Have questions or need more support? Contact our [sales team](https://medusajs.com/contact/).
- Facing issues in your development? Refer to our [troubleshooting guides](https://docs.medusajs.com/resources/troubleshooting/index.html.md).


# Worker Mode of Medusa Instance

In this chapter, you'll learn about the different modes of running a Medusa instance and how to configure the mode.

## What is Worker Mode?

By default, the Medusa application runs 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:

1. A server instance that handles incoming requests to the application's API routes.
2. A worker instance that processes background tasks. This includes scheduled jobs and subscribers.

You don't need to set up different projects for each instance. Instead, you can configure the Medusa application to run in different modes based on environment variables, as you'll see later in this chapter.

This separation ensures that the server instance remains responsive to incoming requests, while the worker instance processes tasks in the background.

![Medusa worker mode architecture diagram illustrating the separation of responsibilities: the server instance handling HTTP requests, API calls, and real-time operations while the dedicated worker instance processes background tasks like data imports, email sending, and resource-intensive operations to maintain optimal server performance](https://res.cloudinary.com/dza7lstvk/image/upload/fl_lossy/f_auto/r_16/ar_16:9,c_pad/v1/Medusa%20Book/medusa-worker_klkbch.jpg?_a=BATFJtAA0)

***

## How to Set Worker Mode

You can set the worker mode of your application using the `projectConfig.workerMode` configuration in the `medusa-config.ts`. The `workerMode` configuration accepts the following values:

- `shared`: (default) run the application in a single process, meaning the worker and server run in the same process.
- `worker`: run a worker process only.
- `server`: run the application server only.

Instead of creating different projects with different worker mode configurations, you can set the worker mode using an environment variable. Then, the worker mode configuration will change based on the environment variable.

For example, set the worker mode in `medusa-config.ts` to the following:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    workerMode: process.env.WORKER_MODE || "shared",
    // ...
  },
  // ...
})
```

You set the worker mode configuration to the `process.env.WORKER_MODE` environment variable and set a default value of `shared`.

Then, in the deployed server Medusa instance, set `WORKER_MODE` to `server`, and in the worker Medusa instance, set `WORKER_MODE` to `worker`:

### Server Medusa Instance

```bash
WORKER_MODE=server
```

### Worker Medusa Instance

```bash
WORKER_MODE=worker
```

### Disable Admin in Worker Mode

Since the worker instance only processes background tasks, you should disable the admin interface in it. That will save resources in the worker instance.

To disable the admin interface, set the `admin.disable` configuration in the `medusa-config.ts` file:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  admin: {
    disable: process.env.ADMIN_DISABLED === "true" ||
      false,
  },
  // ...
})
```

Similar to before, you set the value in an environment variable, allowing you to enable or disable the admin interface based on the environment.

Then, in the deployed server Medusa instance, set `ADMIN_DISABLED` to `false`, and in the worker Medusa instance, set `ADMIN_DISABLED` to `true`:

### Server Medusa Instance

```bash
ADMIN_DISABLED=false
```

### Worker Medusa Instance

```bash
ADMIN_DISABLED=true
```

***

## 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

The Medusa Admin supports multiple languages, with the default being English. In this documentation, you'll learn how to contribute to the community by translating the Medusa Admin to a language you're fluent in.

{/* vale docs.We = NO */}

You can contribute either by translating the admin to a new language, or fixing translations for existing languages. As we can't validate every language's translations, some translations may be incorrect. Your contribution is welcome to fix any translation errors you find.

{/* vale docs.We = YES */}

Check out the translated languages either in the admin dashboard's settings or on [GitHub](https://github.com/medusajs/medusa/blob/develop/packages/admin/dashboard/src/i18n/languages.ts).

***

## How to Contribute Translation

1. Clone the [Medusa monorepository](https://github.com/medusajs/medusa) to your local machine:

```bash
git clone https://github.com/medusajs/medusa.git
```

If you already have it cloned, make sure to pull the latest changes from the `develop` branch.

2. Install the monorepository's dependencies. Since it's a Yarn workspace, it's highly recommended to use yarn:

```bash
yarn install
```

3. Create a branch that you'll use to open the pull request later:

```bash
git checkout -b feat/translate-<LANGUAGE>
```

Where `<LANGUAGE>` is your language name. For example, `feat/translate-da`.

4. Translation files are under `packages/admin/dashboard/src/i18n/translations` as JSON files whose names are the ISO-2 name of the language.
   - If you're adding a new language, copy the file `packages/admin/dashboard/src/i18n/translations/en.json` and paste it with the ISO-2 name for your language. For example, if you're adding Danish translations, copy the `en.json` file and paste it as `packages/admin/dashboard/src/i18n/translations/de.json`.
   - If you're fixing a translation, find the JSON file of the language under `packages/admin/dashboard/src/i18n/translations`.

5. Start translating the keys in the JSON file (or updating the targeted ones). All keys in the JSON file must be translated, and your PR tests will fail otherwise.
   - You can check whether the JSON file is valid by running the following command in `packages/admin/dashboard`, replacing `da.json` with the JSON file's name:

```bash title="packages/admin/dashboard"
yarn i18n:validate da.json
```

6. After finishing the translation, if you're adding a new language, import its JSON file in `packages/admin/dashboard/src/i18n/translations/index.ts` and add it to the exported object:

```ts title="packages/admin/dashboard/src/i18n/translations/index.ts" highlights={[["2"], ["6"], ["7"], ["8"]]}
// other imports...
import da from "./da.json"

export default {
  // other languages...
  da: {
    translation: da,
  },
}
```

The language's key in the object is the ISO-2 name of the language.

7. If you're adding a new language, add it to the file `packages/admin/dashboard/src/i18n/languages.ts`:

```ts title="packages/admin/dashboard/src/i18n/languages.ts" highlights={languageHighlights}
import { da } from "date-fns/locale"
// other imports...

export const languages: Language[] = [
  // other languages...
  {
    code: "da",
    display_name: "Danish",
    ltr: true,
    date_locale: da,
  },
]
```

`languages` is an array having the following properties:

- `code`: The ISO-2 name of the language. For example, `da` for Danish.
- `display_name`: The language's name to be displayed in the admin.
- `ltr`: Whether the language supports a left-to-right layout. For example, set this to `false` for languages like Arabic.
- `date_locale`: An instance of the locale imported from the [date-fns/locale](https://date-fns.org/) package.

8. Once you're done, push the changes into your branch and open a pull request on GitHub.

Our team will perform a general review on your PR and merge it if no issues are found. The translation will be available in the admin after the next release.


# Docs Contribution Guidelines

Thank you for your interest in contributing to the documentation! You will be helping the open source community and other developers interested in learning more about Medusa and using it.

Making a quick fix to a page's content? You can scroll down to the bottom of the page and click on the "Edit this page" link to make quick edits directly in GitHub. If you're making bigger changes, such as adding a new page or making changes to the codebase, check out the rest of this guide.

This guide is specific to contributing to the documentation. If you’re interested in contributing to Medusa’s codebase, check out the [contributing guidelines in the Medusa GitHub repository](https://github.com/medusajs/medusa/blob/develop/CONTRIBUTING.md).

## What Can You Contribute?

You can contribute to the Medusa documentation in the following ways:

- Fixes to existing content. This includes small fixes like typos, or adding missing information.
- Additions to the documentation. If you think a documentation page can be useful to other developers, you can contribute by adding it.
  - Make sure to open an issue first in the [medusa repository](https://github.com/medusajs/medusa) to confirm that you can add that documentation page.
- Fixes to UI components and tooling. If you find a bug while browsing the documentation, you can contribute by fixing it.

***

## Documentation Workspace

Medusa's documentation projects are all part of the documentation `yarn` workspace, which you can find in the [medusa repository](https://github.com/medusajs/medusa) under the `www` directory.

The workspace has the following two directories:

- `apps`: this directory holds the different documentation websites and projects. All projects are built with [Next.js 15](https://nextjs.org/).
  - `book`: includes the codebase and content for the [main Medusa documentation](https://docs.medusajs.com/learn/index.html.md).
  - `resources`: includes the codebase and content for the resources documentation, which powers different sections of the docs such as the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) or [How-to & Tutorials](https://docs.medusajs.com/resources/how-to-tutorials/index.html.md) sections.
  - `api-reference`: includes the codebase and content for the [API reference](https://docs.medusajs.com/api/store).
  - `ui`: includes the codebase and content for the [Medusa UI documentation](https://docs.medusajs.com/ui/index.html.md).
  - `user-guide`: includes the codebase and content for the [user guide documentation](https://docs.medusajs.com/user-guide/index.html.md).
  - `cloud`: includes the codebase and content for the [Medusa Cloud documentation](https://docs.medusajs.com/cloud/index.html.md).
- `packages`: this directory holds the shared packages and components necessary for the development of the projects in the `apps` directory.
  - `docs-ui` includes the shared React components between the different apps.
  - `remark-rehype-plugins` includes Remark and Rehype plugins used by the documentation projects.

### Setup the Documentation Workspace

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Yarn v3+](https://v3.yarnpkg.com/getting-started/install)

In the `www` directory, run the following command to install the dependencies:

```bash
yarn install
```

Then, run the following command to build packages under the `www/packages` directory:

```bash
yarn build
```

After that, you can change into the directory of any documentation project under the `www/apps` directory and run the `dev` command to start the development server.

***

## Documentation Content

All documentation projects are built with Next.js. The content is writtin in MDX files.

### Medusa Main Docs Content

The content of the Medusa main docs are under the `www/apps/book/app` directory.

### Medusa Resources Content

The content of all pages under the `/resources` path are under the `www/apps/resources/app` directory.

Documentation pages under the `www/apps/resources/references` directory are generated automatically from the source code under the `packages/medusa` directory. So, you can't directly make changes to them. Instead, you'll have to make changes to the comments in the original source code.

### API Reference

The API reference's content is split into two types:

1. Static content, which are the content related to getting started, expanding fields, and more. These are located in the `www/apps/api-reference/markdown` directory. They are MDX files.
2. OpenAPI specs that are shown to developers when checking the reference of an API Route. These are generated from OpenApi Spec comments, which are under the `www/utils/generated/oas-output` directory.

### Medusa UI Documentation

The content of the Medusa UI documentation are located under the `www/apps/ui/app` directory. They are MDX files.

The UI documentation also shows code examples, which are under the `www/apps/ui/specs/examples` directory.

The UI component props are generated from the source code and placed into the `www/apps/ui/specs/components` directory. To contribute to these props and their comments, check the comments in the source code under the `packages/design-system/ui` directory.

### Medusa User Guide

The content of all pages under the `/user-guide` path are under the `www/apps/user-guide/app` directory.

### Medusa Cloud Docs

The content of all pages under the `/cloud` path are under the `www/apps/cloud/app` directory.

***

## Run Documentation Projects Locally

To run a documentation project locally, such as `book` or `resources`, change into the directory of that project.

Then, copy the `.env.example` file to a new `.env.local` file. This should sufficiently set up the environment variables needed to run the project.

Finally, run the `dev` command to start the development server. For example, in the `book` project:

```bash npm2yarn
npm run dev
```

You can then access the documentation site by going to the following URLs in your browser:

- `book`: `http://localhost:3000`
- `resources`: `http://localhost:3000/resources`
- `api-reference`: `http://localhost:3000/api` (Store API routes at `/api/store` and Admin API routes at `/api/admin`)
- `ui`: `http://localhost:3000/ui`
- `user-guide`: `http://localhost:3000/user-guide`
- `cloud`: `http://localhost:3000/cloud`

When you access the documentation site, you might see errors or warning messages related to missing environment variables of services like Algolia. You can ignore these errors and warnings when working locally.

### Prep Command

Each documentation project has a `prep` command that runs scripts to prepare data useful for development or production.

The common task that `prep` does is generate the sidebar files, which is useful for both development and production. If you make changes to sidebar items, make sure to run the `prep` command before starting the development server.

```bash npm2yarn
npm run prep
```

In the `resources` project, you may need to run `prep:turbo --force` for tag changes:

```bash npm2yarn title="www/apps/resources"
npm run prep:turbo -- --force
```

***

## Style Guide

When you contribute to the documentation content, make sure to follow the [documentation style guide](https://www.notion.so/Style-Guide-Docs-fad86dd1c5f84b48b145e959f36628e0).

***

## How to Contribute

If you’re fixing small errors in an existing documentation page, you can scroll down to the end of the page and click on the “Edit this page” link. You’ll be redirected to the GitHub edit form of that page and you can make edits directly and submit a pull request (PR).

If you’re adding a new page or contributing to the codebase, fork the repository, create a new branch, and make all changes necessary in your repository. Then, once you’re done, create a PR in the Medusa repository.

### Base Branch

When you make an edit to an existing documentation page or fork the repository to make changes to the documentation, create a new branch.

Documentation contributions always use `develop` as the base branch. Make sure to also open your PR against the `develop` branch.

### Branch Name

Make sure that the branch name starts with `docs/`. For example, `docs/fix-services`. Vercel deployed previews are only triggered for branches starting with `docs/`.

### Pull Request Conventions

When you create a pull request, prefix the title with `docs:` or `docs(PROJECT_NAME):`, where `PROJECT_NAME` is the name of the documentation project this pull request pertains to. For example, `docs(ui): fix titles`.

In the body of the PR, explain clearly what the PR does. If the PR solves an issue, use [closing keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) with the issue number. For example, `Closes #1333`.

#### Prep Command Before PR

If you made changes to the content of any documentation project, you must run the [prep](#prep-command) command in the `book` project before creating a pull request. It will generate the `llms-full.txt` file with the updated content. You should also run it in the project you made changes to, as that will generate other necessary files for the project, such as edit dates.

```bash npm2yarn title="www/apps/book"
npm run prep
```

Some files are ignored by the script. So, if you don't see that the `llms-full.txt` file is updated after running the `prep` command, you can safely ignore it.

***

## Images

If you are adding images to a documentation page, you can host the image on [Imgur](https://imgur.com) for free to include it in the PR. Our team will later upload it to our image hosting CDN.

***

## NPM and Yarn Code Blocks

If you’re adding code blocks that use NPM and Yarn, you must add the `npm2yarn` meta field.

For example:

````md
```bash npm2yarn
npm run start
```
````

The code snippet must be written using NPM.

### Global Option

When a command uses the global option `-g`, add it at the end of the NPM command to ensure that it’s transformed to a Yarn command properly. For example:

```bash npm2yarn
npm install @medusajs/cli -g
```

***

## Linting with Vale

Medusa uses [Vale](https://vale.sh/) to lint documentation pages and perform checks on incoming PRs into the repository.

### Result of Vale PR Checks

You can check the result of running the "lint" action on your PR by clicking the Details link next to it. You can find there all errors that you need to fix.

### Run Vale Locally

If you want to check your work locally, you can do that by:

1. [Installing Vale](https://vale.sh/docs/vale-cli/installation/) on your machine.
2. Changing to the `www/vale` directory:

```bash
cd www/vale
```

3\. Running the `run-vale` script:

```bash
# to lint content for the main documentation
./run-vale.sh book/app/learn error resources
# to lint content for the resources documentation
./run-vale.sh resources/app error
# to lint content for the API reference
./run-vale.sh api-reference/markdown error
# to lint content for the Medusa UI documentation
./run-vale.sh ui/app error
# to lint content for the user guide
./run-vale.sh user-guide/app error
```

{/* TODO need to enable MDX v1 comments first. */}

{/* ### Linter Exceptions

If it's needed to break some style guide rules in a document, you can wrap the parts that the linter shouldn't scan with the following comments in the `md` or `mdx` files:

```md
<!-- vale off -->

content that shouldn't be scanned for errors here...

<!-- vale on -->
```

You can also disable specific rules. For example:

```md
<!-- vale docs.Numbers = NO -->

Medusa supports Node versions 14 and 16.

<!-- vale docs.Numbers = YES -->
```

If you use this in your PR, you must justify its usage. */}

***

## Linting with ESLint

Medusa uses ESlint to lint code blocks both in the content and the code base of the documentation apps.

### Linting Content with ESLint

Each PR runs through a check that lints the code in the content files using ESLint. The action's name is `content-eslint`.

If you want to check content ESLint errors locally and fix them, you can do that by:

1\. Install the dependencies in the `www` directory:

```bash
yarn install
```

2\. Run the turbo command in the `www` directory or the specific documentation's directory (for example, `www/apps/book`):

```bash
turbo run lint:content
```

This will fix any fixable errors, and show errors that require your action.

### Linting Code with ESLint

Each PR runs through a check that lints the code in the content files using ESLint. The action's name is `code-docs-eslint`.

If you want to check code ESLint errors locally and fix them, you can do that by:

1\. Install the dependencies in the `www` directory:

```bash
yarn install
```

2\. Run the turbo command in the `www` directory or the specific documentation's directory (for example, `www/apps/book`):

```bash
yarn lint
```

This will fix any fixable errors, and show errors that require your action.

{/* TODO need to enable MDX v1 comments first. */}

{/* ### ESLint Exceptions

If some code blocks have errors that can't or shouldn't be fixed, you can add the following command before the code block:

~~~md
<!-- eslint-skip -->

```js
console.log("This block isn't linted")
```

```js
console.log("This block is linted")
```
~~~

You can also disable specific rules. For example:

~~~md
<!-- eslint-disable semi -->

```js
console.log("This block can use semicolons");
```

```js
console.log("This block can't use semi colons")
```
~~~ */}


# Usage Information

At Medusa, we strive to provide the best experience for developers using our platform. For that reason, Medusa collects anonymous and non-sensitive data that provides a global understanding of how users are using Medusa.

***

## Purpose

As an open source solution, we work closely and constantly interact with our community to ensure that we provide the best experience for everyone using Medusa.

We are capable of getting a general understanding of how developers use Medusa and what general issues they run into through different means such as our Discord server, GitHub issues and discussions, and occasional one-on-one sessions.

However, although these methods can be insightful, they’re not enough to get a full and global understanding of how developers are using Medusa, especially in production.

Collecting this data allows us to understand certain details such as:

- What operating system do most Medusa developers use?
- What version of Medusa is widely used?
- What parts of the Medusa Admin are generally undiscovered by our users?
- How much data do users manage through our Medusa Admin? Is it being used for large number of products, orders, and other types of data?
- What Node version is globally used? Should we focus our efforts on providing support for versions that we don’t currently support?

***

## Medusa Application Analytics

This section covers which data in the Medusa application are collected and how to opt out of it.

### Collected Data in the Medusa Application

The following data is being collected on your Medusa application:

- Unique project ID generated with UUID.
- Unique machine ID generated with UUID.
- Operating system information including Node version or operating system platform used.
- The version of the Medusa application and Medusa CLI are used.

Data is only collected when the Medusa application is run with the command `medusa start`.

### How to Opt Out

If you prefer to disable data collection, you can do it either by setting the following environment variable to true:

```bash
MEDUSA_DISABLE_TELEMETRY=true
```

Or, you can run the following command in the root of your Medusa application project to disable it:

```bash
npx medusa telemetry --disable
```

***

## Admin Analytics

This section covers which data in the admin are collected and how to opt out of it.

### Collected Data in Admin

Users have the option to [enable or disable the anonymization](#how-to-enable-anonymization) of the collected data.

The following data is being collected on your admin:

- The name of the store.
- The email of the user.
- The total number of products, orders, discounts, and users.
- The number of regions and their names.
- The currencies used in the store.
- Errors that occur while using the admin.

### How to Enable Anonymization

To enable anonymization of your data from the Medusa Admin:

1. Go to Settings → Personal Information.
2. In the Usage insights section, click on the “Edit preferences” button.
3. Enable the "Anonymize my usage data” toggle.
4. Click on the “Submit and close” button.

### How to Opt-Out

To opt out of analytics collection in the Medusa Admin, set the following environment variable:

```bash
MEDUSA_FF_ANALYTICS=false
```


# Storefront Development

In this chapter, you'll learn about storefronts and how to build one.

## Storefronts in Medusa

The Medusa application includes a Node.js server and an admin dashboard. Storefronts are separate applications that you install, build, and host independently from Medusa. This gives you the flexibility to choose your preferred frontend tech stack and implement unique designs and user experiences.

In your storefront, you can retrieve data and perform commerce operations by sending requests to the Medusa application's [Store API routes](https://docs.medusajs.com/api/store) and your custom API routes.

You can build your storefront from scratch with your preferred tech stack, or start with our Next.js Starter storefront. The Next.js Starter storefront provides rich commerce features and a modern design. You can use it as-is or customize it to match your business needs, design requirements, and customer experience goals.

- [Install Next.js Starter Storefront](https://docs.medusajs.com/resources/nextjs-starter/index.html.md)
- [Build Custom Storefront](https://docs.medusajs.com/resources/storefront-development/index.html.md)

***

## Using a Publishable API Key in Storefront Requests

When sending requests to API routes that start with `/store`, you must include a publishable API key in the request header.

A publishable API key sets the scope of your request to one or more sales channels.

When you retrieve products, only products from those sales channels are returned. This also ensures you get correct inventory data and associate created orders with the right sales channel.

Learn more about using the publishable API key in the [Storefront Development guide](https://docs.medusajs.com/resources/storefront-development/publishable-api-keys/index.html.md).


# Updating Medusa

In this chapter, you'll learn about updating your Medusa application and packages.

Medusa's current version is v{config.version.number}. {releaseNoteText}

## Medusa Versioning

When Medusa puts out a new release, all packages are updated to the same version. This ensures that all packages are compatible with each other, and makes it easier for you to switch between versions.

This doesn't apply to the design-system packages, including `@medusajs/ui`, `@medusajs/ui-presets`, and `@medusajs/ui-icons`. These packages are versioned independently. However, you don't need to install and manage them separately in your Medusa application, as they are included in the `@medusajs/admin-sdk`. If you're using them in a standalone project, such as a storefront or custom admin dashboard, refer to [this section in the Medusa UI documentation](https://docs.medusajs.com/ui/installation/standalone-project#updating-ui-packages/index.html.md) for update instructions.

Medusa updates the version number `major.minor.patch` according to the following rules:

- **patch**: A patch release includes bug fixes and minor improvements. It doesn't include breaking changes. For example, if the current version is `2.0.0`, the next patch release will be `2.0.1`.
- **minor**: A minor release includes new features, fixes, improvements, and breaking changes. For example, if the current version is `2.0.0`, the next minor release will be `2.1.0`.
- **major**: A major release includes significant changes to the entire codebase and architecture. For those, the update process will be more elaborate. For example, if the current version is `2.0.0`, the next major release would be `3.0.0`.

***

## Check Installed Version

To check the currently installed version of Medusa in your project, run the following command in your Medusa application:

```bash
npx medusa -v
```

This will show you the installed version of Medusa and the [Medusa CLI tool](https://docs.medusajs.com/resources/medusa-cli/index.html.md), which should be the same.

***

## Check Latest Version

The documentation shows the current version at the top right of the navigation bar. When a new version is released, you'll find a blue dot on the version number. Clicking it will take you to the [release notes on GitHub](https://github.com/medusajs/medusa/releases).

You can also star the [Medusa repository on GitHub](https://github.com/medusajs/medusa) to receive updates about new releases on your GitHub dashboard. Our team also shares updates on new releases on our social media channels.

***

## Update Medusa Application

Before updating a Medusa application, make sure to check the [release notes](https://github.com/medusajs/medusa/releases) for any breaking changes that require actions from your side.

Then, to update your Medusa application, bump the version of all `@medusajs/*` dependencies in your `package.json`. Then, re-install dependencies:

```bash npm2yarn
npm install
```

This will update all Medusa packages to the latest version.

### Running Migrations

Releases may include changes to the database, such as new tables, updates to existing tables, updates after adding links, or data migration scripts.

So, after updating Medusa, run the following command to migrate the latest changes to your database:

```bash
npx medusa db:migrate
```

This will run all pending migrations, sync links, and run data migration scripts.

### Reverting an Update

Before reverting an update, if you already ran the migrations, you have to first identify the modules who had migrations. Then, before reverting, run the `db:rollback` command for each of those modules.

For example, if the version you updated to had migrations for the Cart and Product Modules, run the following command:

```bash
npx medusa db:rollback cart product
```

Then, revert the update by changing the version of all `@medusajs/*` dependencies in your `package.json` to the previous version and re-installing dependencies:

```bash npm2yarn
npm  install
```

Finally, run the migrations to sync link changes:

```bash
npx medusa db:migrate
```

***

## Understanding Codebase Changes

In the Medusa codebase, our team uses the following [TSDoc](https://tsdoc.org/) tags to indicate changes made in the latest version for a specific piece of code:

- `@deprecated`: Indicates that a piece of code is deprecated and will be removed in a future version. The tag's message will include details on what to use instead. However, our updates are always backward-compatible, allowing you to update your codebase at your own pace.
- `@since`: Indicates the version when a piece of code was available from. A piece of code that has this tag will only be available starting from the specified version.

***

## Update Plugin Project

If you have a Medusa plugin project, you only need to update its `@medusajs/*` dependencies in the `package.json` file to the latest version. Then, re-install dependencies:

```bash npm2yarn
npm  install
```


# API Key Concepts

In this guide, you’ll learn about the different types of API keys, their expiration and verification.

## API Key Types

There are two types of API keys:

- `publishable`: A public key used in client applications, such as a storefront.
  - This API key is useful for operations that do not require authentication, such as fetching product data or categories.
- `secret`: A secret key used for authentication and verification purposes, such as an admin user’s authentication token or a password reset token.
  - This API key is useful for operations that require authentication, such as creating orders or managing products as an admin user.

The API key’s type is stored in the `type` property of the [ApiKey data model](https://docs.medusajs.com/references/api-key/models/ApiKey/index.html.md).

### Default Scopes and Permissions

In your Medusa application, a `publishable` API key is only useful to send requests to the [Store API routes](https://docs.medusajs.com/api/store). Learn more about it in the [Publishable API Keys](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md) guide.

In addition, a `secret` API key allows you to access the [Admin API routes](https://docs.medusajs.com/api/admin) and perform actions as the admin user that the key was created for. The `created_by` property of the [ApiKey data model](https://docs.medusajs.com/references/api-key/models/ApiKey/index.html.md) indicates the ID of the associated admin user.

***

## API Key Creation

When using the [Medusa Admin](https://docs.medusajs.com/user-guide/settings/developer/index.html.md) or [API routes](https://docs.medusajs.com/api/admin#api-keys), only admin users can create API keys.

You can also create API keys in your customizations using the [createApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/createApiKeysWorkflow/index.html.md).

***

## API Key Tokens

The API key data model has a `token` property that contains the actual key used for authentication.

This token is created using the `salt` property in the data model, which is a random string generated when the API key is created. The salt is a `64`-character hexadecimal string generated randomly using the `crypto` module in Node.js.

For display purposes, the API key data model also has a `redacted` property that contains the first six characters of the token, followed by `...`, then the last three characters of the token. You can use this property to show the API key in the UI without revealing the full token.

***

## API Key Expiration

An API key expires when it’s revoked using the [revokeApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/revokeApiKeysWorkflow/index.html.md). This method will set the following properties in the API key:

- `revoked_at`: The date and time when the API key was revoked.
- `revoked_by`: The ID of the user who revoked the API key.

The associated token is no longer usable or verifiable.

***

## Token Verification

To verify a token received as an input or in a request, use the [authenticate method of the module’s main service](https://docs.medusajs.com/references/api-key/authenticate/index.html.md) which validates the token against all non-expired tokens.


# Links between API Key Module and Other Modules

This document showcases the module links defined between the API Key Module and other Commerce Modules.

## Summary

The API Key Module has the following links to other modules:

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|ApiKey|SalesChannel|Stored - many-to-many|Learn more|

***

## Sales Channel Module

You can create a publishable API key and associate it with a sales channel. Medusa defines a link between the `ApiKey` and the `SalesChannel` data models.

![A diagram showcasing an example of how data models from the API Key and Sales Channel modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709812064/Medusa%20Resources/sales-channel-api-key_zmqi2l.jpg)

This is useful to avoid passing the sales channel's ID as a parameter of every request, and instead pass the publishable API key in the header of any request to the Store API route.

Learn more about this in the [Sales Channel Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md).

### Retrieve with Query

To retrieve the sales channels of an API key with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`:

### query.graph

```ts
const { data: apiKeys } = await query.graph({
  entity: "api_key",
  fields: [
    "sales_channels.*",
  ],
})

// apiKeys[0].sales_channels
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: apiKeys } = useQueryGraphStep({
  entity: "api_key",
  fields: [
    "sales_channels.*",
  ],
})

// apiKeys[0].sales_channels
```

### Manage with Link

To manage the sales channels of an API key, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.API_KEY]: {
    publishable_key_id: "apk_123",
  },
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.API_KEY]: {
    publishable_key_id: "apk_123",
  },
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
})
```


# API Key Module

In this section of the documentation, you will find resources to learn more about the API Key Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/developer/index.html.md) to learn how to manage publishable and secret API keys using the dashboard.

Medusa has API-key related features available out-of-the-box through the API Key Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this API Key Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## API Key Features

- [API Key Types and Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts/index.html.md): Manage API keys in your store. You can create both publishable and secret API keys for different use cases.
- [Token Verification](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts#token-verification/index.html.md): Verify tokens of secret API keys to authenticate users or actions.
- [Revoke Keys](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts#api-key-expiration/index.html.md): Revoke keys to disable their use permanently.
- Roll API Keys: Roll API keys by [revoking](https://docs.medusajs.com/references/api-key/revoke/index.html.md) a key then [re-creating it](https://docs.medusajs.com/references/api-key/createApiKeys/index.html.md).

***

## How to Use the API Key Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-api-key.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createApiKeyStep = createStep(
  "create-api-key",
  async ({}, { container }) => {
    const apiKeyModuleService = container.resolve(Modules.API_KEY)

    const apiKey = await apiKeyModuleService.createApiKeys({
      title: "Publishable API key",
      type: "publishable",
      created_by: "user_123",
    })

    return new StepResponse({ apiKey }, apiKey.id)
  },
  async (apiKeyId, { container }) => {
    const apiKeyModuleService = container.resolve(Modules.API_KEY)

    await apiKeyModuleService.deleteApiKeys([apiKeyId])
  }
)

export const createApiKeyWorkflow = createWorkflow(
  "create-api-key",
  () => {
    const { apiKey } = createApiKeyStep()

    return new WorkflowResponse({
      apiKey,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createApiKeyWorkflow } from "../../workflows/create-api-key"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createApiKeyWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createApiKeyWorkflow } from "../workflows/create-api-key"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createApiKeyWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createApiKeyWorkflow } from "../workflows/create-api-key"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createApiKeyWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Authentication Flows with the Auth Main Service

In this guide, you'll learn how to use the methods of the Auth Module's service to implement authentication flows and reset a user's password.

This guide is only recommended for complex cases, where you may need to heavily customize the authentication flows and need to use the Auth Module's service methods directly. For most use cases, you should use the [auth routes](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md) instead.

## Authentication Methods

### Register

The [register method of the Auth Module's service](https://docs.medusajs.com/references/auth/register/index.html.md) creates an auth identity that can be authenticated later.

For example:

```ts
const data = await authModuleService.register(
  "emailpass",
  // passed to auth provider
  {
    // pass data from request parameters, such as
    url: "example.com",
    headers: req.headers,
    query: req.query,
    // pass in the body the data necessary for the authentication provider
    // to register the user, such as email and password
    body: {
      email: "user@example.com",
      password: "supersecret",
    },
    protocol: req.protocol,
  }
)
```

This method calls the `register` method of the provider specified in the first parameter. The method passes the second parameter as input for the provider, and returns its data.

Based on the returned data, you can determine if the registration was successful, and if any further action is required to complete the registration. This is explained further in the auth flows sections below.

### Authenticate

To authenticate or log in a user, you use the [authenticate method of the Auth Module's service](https://docs.medusajs.com/references/auth/authenticate/index.html.md). For example:

```ts
const data = await authModuleService.authenticate(
  "emailpass",
  // passed to auth provider
  {
    // pass data from request parameters, such as
    url: "example.com",
    headers: req.headers,
    query: req.query,
    // pass in the body the data necessary for the authentication provider
    // to authenticate the user, such as email and password
    body: {
      email: "user@example.com",
      password: "supersecret",
    },
    protocol: req.protocol,
  }
)
```

This method calls the `authenticate` method of the provider specified in the first parameter. The method passes the second parameter as input for the provider, and returns its data.

Based on the returned data, you can determine if the authentication was successful, and if any further action is required to complete the authentication. This is explained further in the auth flows sections below.

***

## Auth Flow 1: Basic Authentication

The basic authentication flow requires first using the `register` method, then the `authenticate` method.

![Diagram showcasing the basic authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711373749/Medusa%20Resources/basic-auth_lgpqsj.jpg)

### Step 1: Register User

```ts
const { 
  success, 
  authIdentity, 
  error,
} = await authModuleService.register(
  "emailpass",
  // passed to auth provider
  {
    // pass data from request parameters, such as
    url: "example.com",
    headers: req.headers,
    query: req.query,
    // pass in the body the data necessary for the authentication provider
    // to register the user, such as email and password
    body: {
      email: "user@example.com",
      password: "supersecret",
    },
    protocol: req.protocol,
  }
)

if (error) {
  // registration failed
  // TODO return an error
  return
}
```

If the `register` method returns an `error` property, the registration failed, and you can return an error message to the user.

Otherwise, if the `success` property is `true`, the registration was successful, and the user's authentication details are available within the `authIdentity` object.

Check out the [AuthIdentity](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) reference for the received properties in `authIdentity`.

#### Registering Auth Identities with Same Identifier

If an auth identity, such as a `customer`, tries to register with an email of another auth identity, the `register` method returns an error. This can happen either if another customer is using the same email, or an admin user has the same email.

There are two ways to handle this:

- Consider the customer authenticated if the `authenticate` method validates that the email and password are correct. This allows admin users, for example, to authenticate as customers.
- Return an error message to the customer, informing them that the email is already in use.

### Step 2: Authenticate User

```ts
// later (can be another route for login)
const { 
  success, 
  authIdentity, 
  location,
} = await authModuleService.authenticate(
  "emailpass",
  // passed to auth provider
  {
    // pass data from request parameters, such as
    url: "example.com",
    headers: req.headers,
    query: req.query,
    // pass in the body the data necessary for the authentication provider
    // to authenticate the user, such as email and password
    body: {
      email: "user@example.com",
      password: "supersecret",
    },
    protocol: req.protocol,
  }
)

if (success && !location) {
  // user is authenticated
}
```

If the `authenticate` method returns a `success` property that is `true`, and the `location` property is not set, the user is authenticated successfully. Their authentication details are available within the `authIdentity` object.

Otherwise, if the `location` property is set, you must follow the [third-party service authentication flow](#auth-flow-2-third-party-service-authentication) to complete the authentication.

***

## Auth Flow 2: Third-Party Service Authentication

The third-party service authentication method requires using the `authenticate` method first:

```ts
const { success, authIdentity, location } = await authModuleService.authenticate(
  "google",
  // passed to auth provider
  {
    // pass data from request parameters, such as
    url: "example.com",
    headers: req.headers,
    query: req.query,
    body: req.body,
    protocol: req.protocol,
  }
)

if (location) {
  // return the location for the frontend to redirect to
}

if (!success) {
  // authentication failed
}

// authentication successful
```

If the `authenticate` method returns a `location` property, the authentication process requires the user to perform an action with a third-party service. So, you return the `location` to the frontend or client to redirect to that URL.

For example, when using the `google` provider, the `location` is the URL that the user must be redirected to in order to log in with their Google account.

![Diagram showcasing the first part of the third-party authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711374847/Medusa%20Resources/third-party-auth-1_enyedy.jpg)

### Overriding Callback URL

The Google and GitHub providers allow you to override their `callbackUrl` option during authentication. This is useful when you redirect the user after authentication to a URL based on their actor type. For example, you redirect admin users and customers to different pages.

```ts
const { 
  success, 
  authIdentity, 
  location,
} = await authModuleService.authenticate(
  "google",
  // passed to auth provider
  {
    // ...
    callback_url: "example.com",
  }
)
```

### validateCallback

Providers handling this authentication flow must implement the `validateCallback` method. It implements the logic to validate the authentication with the third-party service.

So, once the user performs the required action with the third-party service (for example, log in with Google), the frontend must redirect to an API route that uses the [validateCallback method of the Auth Module's service](https://docs.medusajs.com/references/auth/validateCallback/index.html.md).

The method calls the specified provider’s `validateCallback` method passing it the authentication details it received in the second parameter:

```ts
const { success, authIdentity } = await authModuleService.validateCallback(
  "google",
  // passed to auth provider
  {
    // request data, such as
    url,
    headers,
    query,
    body,
    protocol,
  }
)

if (success) {
  // authentication succeeded
}
```

For providers like Google, the `query` object contains the query parameters from the original callback URL, such as the `code` and `state` parameters.

If the returned `success` property is `true`, the authentication with the third-party provider was successful.

![Diagram showcasing the second part of the third-party authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711375123/Medusa%20Resources/third-party-auth-2_kmjxju.jpg)

***

## Reset Password

To update a user's password or other authentication details, use the [updateProvider method of the Auth Module's service](https://docs.medusajs.com/references/auth/updateProvider/index.html.md). It calls the `update` method of the specified authentication provider.

For example:

```ts
const { success } = await authModuleService.updateProvider(
  "emailpass",
  // passed to the auth provider
  {
    entity_id: "user@example.com",
    password: "supersecret",
  }
)

if (success) {
  // password reset successfully
}
```

The method accepts as a first parameter the ID of the provider, and as a second parameter the data necessary to reset the password.

In the example above, you use the `emailpass` provider, so you have to pass an object having `entity_id` and `password` properties.

If the returned `success` property is `true`, the password has been reset successfully.


# Auth Identity and Actor Types

In this guide, you’ll learn about concepts related to identity and actors in the Auth Module.

## What is an Auth Identity?

The [AuthIdentity data model](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) represents a user registered by an [authentication provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/index.html.md). When a user is registered using an authentication provider, the provider creates a record of `AuthIdentity`.

Then, when the user logs in with the same authentication provider, the associated auth identity is used to validate their credentials.

***

## Actor Types

An actor type is a type of user that can be authenticated. The Auth Module doesn't store or manage any user-like models, such as for customers or users. Instead, the user types are created and managed by other modules. For example, a customer is managed by the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md).

When an auth identity is created for an actor type, the ID of the user is stored in the `app_metadata` property of the auth identity.

For example, an auth identity of a customer has the following `app_metadata` property:

```json
{
  "app_metadata": {
    "customer_id": "cus_123"
  }
}
```

The ID of the user is stored in the key `{actor_type}_id` of the `app_metadata` property.

***

## Protect Routes by Actor Type

When you protect routes with the [authenticate middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md), you specify in its first parameter the actor type that must be authenticated to access the API routes.

For example:

```ts title="src/api/middlewares.ts" highlights={highlights}
import { 
  defineMiddlewares,
  authenticate,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom/admin*",
      middlewares: [
        authenticate("user", ["session", "bearer", "api-key"]),
      ],
    },
  ],
})
```

By specifying `user` as the first parameter of `authenticate`, only authenticated users of actor type `user` (admin users) can access API routes starting with `/custom/admin`.

***

## Custom Actor Types

You can define custom actor types that allow a custom user, managed by your custom module, to authenticate into Medusa.

For example, if you have a custom module with a `Manager` data model, you can authenticate managers with the `manager` actor type.

Learn how to create a custom actor type in the [Create Manager Actor Type guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md).


# Emailpass Auth Module Provider

In this guide, you’ll learn about the Emailpass Auth Module Provider and how to configure it.

By using the Emailpass Auth Module Provider, you allow users to register and log in with an email and password.

## Register the Emailpass Auth Module Provider

The Emailpass Auth Module Provider is registered by default with the Auth Module.

If you want to pass options to the provider, add the provider to the `providers` option of the Auth Module in your `medusa-config.ts`:

```ts title="medusa-config.ts"
import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/auth",
      dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER],
      options: {
        providers: [
          // other providers...
          {
            resolve: "@medusajs/medusa/auth-emailpass",
            id: "emailpass",
            options: {
              // options...
            },
          },
        ],
      },
    },
  ],
})
```

### Module Options

|Configuration|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`hashConfig\`|An object of configurations for hashing the user's
password. Refer to |No|\`\`\`ts
const hashConfig = \{
&#x20; logN: 15,
&#x20; r: 8,
&#x20; p: 1
}
\`\`\`|

***

## Related Guides

- [How to register a customer using email and password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md)


# GitHub Auth Module Provider

In this guide, you’ll learn about the GitHub Auth Module Provider and how to configure it.

The GitHub Auth Module Provider allows you to authenticate users with their GitHub account.

## Register the GitHub Auth Module Provider

### Prerequisites

- [Register a GitHub App. When setting the Callback URL, set it to a URL in your frontend that later uses Medusa's callback route to validate the authentication.](https://docs.github.com/en/apps/creating-github-apps/setting-up-a-github-app/creating-a-github-app)
- [Retrieve the client ID and client secret of your GitHub App](https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api?apiVersion=2022-11-28#using-basic-authentication)

To use the GitHub Auth Module Provider, add the module to the array of providers passed to the Auth Module in your `medusa-config.ts`:

```ts title="medusa-config.ts"
import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/auth",
      dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER],
      options: {
        providers: [
          // other providers...
          {
            resolve: "@medusajs/medusa/auth-github",
            id: "github",
            options: {
              clientId: process.env.GITHUB_CLIENT_ID,
              clientSecret: process.env.GITHUB_CLIENT_SECRET,
              callbackUrl: process.env.GITHUB_CALLBACK_URL,
            },
          },
        ],
      },
    },
  ],
})
```

### Environment Variables

Make sure to add the necessary environment variables for the above options in `.env`:

```plain
GITHUB_CLIENT_ID=<YOUR_GITHUB_CLIENT_ID>
GITHUB_CLIENT_SECRET=<YOUR_GITHUB_CLIENT_SECRET>
GITHUB_CALLBACK_URL=<YOUR_GITHUB_CALLBACK_URL>
```

### Module Options

|Configuration|Description|Required|
|---|---|---|---|---|
|\`clientId\`|A string indicating the client ID of your GitHub app.|Yes|
|\`clientSecret\`|A string indicating the client secret of your GitHub app.|Yes|
|\`callbackUrl\`|A string indicating the URL to redirect to in your frontend after the user completes their authentication in GitHub.|Yes|

***

## Override Callback URL During Authentication

In many cases, you may have different callback URLs for actor types. For example, you may redirect admin users to a different URL than customers after authentication.

The [Authenticate or Login API Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md) accepts a `callback_url` body parameter to override the provider's `callbackUrl` option. Learn more in the [Auth Flows with Routes guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md).

***

## Examples

- [How to implement third-party / social login in the storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md)


# Google Auth Module Provider

In this guide, you’ll learn about the Google Auth Module Provider and how to install and use it in the Auth Module.

The Google Auth Module Provider allows you to authenticate users with their Google account.

## Register the Google Auth Module Provider

### Prerequisites

- [Create a project in Google Cloud.](https://cloud.google.com/resource-manager/docs/creating-managing-projects)
- [Create Oauth client ID credentials. When setting the Redirect Uri, set it to a URL in your frontend (for example, storefront) that later uses Medusa's callback route to validate the authentication.](https://developers.google.com/identity/protocols/oauth2/web-server#creatingcred)

To use the Google Auth Module Provider, add the module to the array of providers passed to the Auth Module in your `medusa-config.ts`:

```ts title="medusa-config.ts"
import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    // ...
    {
      resolve: "@medusajs/medusa/auth",
      dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER],
      options: {
        providers: [
          // other providers...
          {
            resolve: "@medusajs/medusa/auth-emailpass",
            id: "emailpass",
          },
          {
            resolve: "@medusajs/medusa/auth-google",
            id: "google",
            options: {
              clientId: process.env.GOOGLE_CLIENT_ID,
              clientSecret: process.env.GOOGLE_CLIENT_SECRET,
              callbackUrl: process.env.GOOGLE_CALLBACK_URL,
            },
          },
        ],
      },
    },
  ],
})
```

### Environment Variables

Make sure to add the necessary environment variables for the above options in `.env`:

```plain
GOOGLE_CLIENT_ID=<YOUR_GOOGLE_CLIENT_ID>
GOOGLE_CLIENT_SECRET=<YOUR_GOOGLE_CLIENT_SECRET>
GOOGLE_CALLBACK_URL=<YOUR_GOOGLE_CALLBACK_URL>
```

### Module Options

|Configuration|Description|Required|
|---|---|---|---|---|
|\`clientId\`|A string indicating the |Yes|
|\`clientSecret\`|A string indicating the |Yes|
|\`callbackUrl\`|A string indicating the URL to redirect to in your frontend after the user completes their authentication in Google.|Yes|

***

## Override Callback URL During Authentication

In many cases, you may have different callback URLs for actor types. For example, you may redirect admin users to a different URL than customers after authentication.

The [Authenticate or Login API Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md) can accept a `callback_url` body parameter to override the provider's `callbackUrl` option. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md).

***

## Examples

- [How to implement Google social login in the storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md)


# Auth Module Provider

In this guide, you’ll learn about the Auth Module Provider and how it's used.

## What is an Auth Module Provider?

An Auth Module Provider handles authenticating customers and users, either using custom logic or by integrating a third-party service.

For example, the EmailPass Auth Module Provider authenticates a user using their email and password, whereas the Google Auth Module Provider authenticates users using their Google account.

### Auth Providers List

- [Emailpass](https://docs.medusajs.com/commerce-modules/auth/auth-providers/emailpass/index.html.md)
- [Google](https://docs.medusajs.com/commerce-modules/auth/auth-providers/google/index.html.md)
- [GitHub](https://docs.medusajs.com/commerce-modules/auth/auth-providers/github/index.html.md)

***

## How to Create an Auth Module Provider?

An Auth Module Provider is a module whose service extends the `AbstractAuthModuleProvider` imported from `@medusajs/framework/utils`.

The module can have multiple auth provider services, where each is registered as a separate auth provider.

Refer to the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider/index.html.md) guide to learn how to create an Auth Module Provider.

***

## Configure Allowed Auth Providers of Actor Types

By default, users of all actor types can authenticate with all installed Auth Module Providers.

To restrict the auth providers used for actor types, use the [authMethodsPerActor option](https://docs.medusajs.com/docs/learn/configurations/medusa-config#httpauthMethodsPerActor/index.html.md) in Medusa's configurations:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      authMethodsPerActor: {
        user: ["google"],
        customer: ["emailpass"],
      },
      // ...
    },
    // ...
  },
})
```

When you specify the `authMethodsPerActor` configuration, it overrides the default. So, if you don't specify any providers for an actor type, users of that actor type can't authenticate with any provider.


# How to Use Authentication Routes

In this document, you'll learn about the authentication routes and how to use them to create and log-in users, and reset their password.

These routes are added by Medusa's HTTP layer, not the Auth Module.

## Types of Authentication Flows

### 1. Basic Authentication Flow

This authentication flow doesn't require validation with third-party services.

[How to register customer in storefront using basic authentication flow](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md).

The steps are:

![Diagram showcasing the basic authentication flow between the frontend and the Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1725539370/Medusa%20Resources/basic-auth-routes_pgpjch.jpg)

1. Register the user with the [Register Route](#register-route).
2. Use the authentication token to create the user with their respective API route.
   - For example, for customers you would use the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers).
   - For admin users, you accept an invite using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept)
3. Authenticate the user with the [Auth Route](#login-route).

After registration, you only use the [Auth Route](#login-route) for subsequent authentication.

To handle errors related to existing identities, refer to [this section](#handling-existing-identities).

### 2. Third-Party Service Authenticate Flow

This authentication flow authenticates the user with a third-party service, such as Google.

[How to authenticate customer with a third-party provider in the storefront.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md).

It requires the following steps:

![Diagram showcasing the authentication flow between the frontend, Medusa application, and third-party service](https://res.cloudinary.com/dza7lstvk/image/upload/v1725528159/Medusa%20Resources/Third_Party_Auth_tvf4ng.jpg)

1. Authenticate the user with the [Auth Route](#login-route).
2. The auth route returns a URL to authenticate with third-party service, such as login with Google. The frontend (such as a storefront), when it receives a `location` property in the response, must redirect to the returned location.
3. Once the authentication with the third-party service finishes, it redirects back to the frontend with a `code` query parameter. So, make sure your third-party service is configured to redirect to your frontend page after successful authentication.
4. The frontend sends a request to the [Validate Callback Route](#validate-callback-route) passing it the query parameters received from the third-party service, such as the `code` and `state` query parameters.
5. If the callback validation is successful, the frontend receives the authentication token.
6. Decode the received token in the frontend using tools like [react-jwt](https://www.npmjs.com/package/react-jwt).
   - If the decoded data has an `actor_id` property, then the user is already registered. So, use this token for subsequent authenticated requests.
   - If not, follow the rest of the steps.
7. The frontend uses the authentication token to create the user with their respective API route.
   - For example, for customers you would use the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers).
   - For admin users, you accept an invite using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept)
8. The frontend sends a request to the [Refresh Token Route](#refresh-token-route) to retrieve a new token with the user information populated.

***

## Register Route

The Medusa application defines an API route at `/auth/{actor_type}/{provider}/register` that creates an auth identity for an actor type, such as a `customer`. It returns a JWT token that you pass to an API route that creates the user.

```bash
curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/register
-H 'Content-Type: application/json' \
--data-raw '{
  "email": "Whitney_Schultz@gmail.com"
  // ...
}'
```

This API route is useful for providers like `emailpass` that uses custom logic to authenticate a user. For authentication providers that authenticate with third-party services, such as Google, use the [Auth Route](#login-route) instead.

For example, if you're registering a customer, you:

1. Send a request to `/auth/customer/emailpass/register` to retrieve the registration JWT token.
2. Send a request to the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers) to create the customer, passing the [JWT token in the header](https://docs.medusajs.com/api/store#authentication).

### Path Parameters

Its path parameters are:

- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`.
- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`.

### Request Body Parameters

This route accepts in the request body the data that the specified authentication provider requires to handle authentication.

For example, the EmailPass provider requires an `email` and `password` fields in the request body.

### Response Fields

If the authentication is successful, you'll receive a `token` field in the response body object:

```json
{
  "token": "..."
}
```

Use that token in the header of subsequent requests to send authenticated requests.

### Handling Existing Identities

An auth identity with the same email may already exist in Medusa. This can happen if:

- Another actor type is using that email. For example, an admin user is trying to register as a customer.
- The same email belongs to a record of the same actor type. For example, another customer has the same email.

In these scenarios, the Register Route will return an error instead of a token:

```json
{
  "type": "unauthorized",
  "message": "Identity with email already exists"
}
```

To handle these scenarios, you can use the [Login Route](#login-route) to validate that the email and password match the existing identity. If so, you can allow the admin user, for example, to register as a customer.

Otherwise, if the email and password don't match the existing identity, such as when the email belongs to another customer, the [Login Route](#login-route) returns an error:

```json
{
  "type": "unauthorized",
  "message": "Invalid email or password"
}
```

You can show that error message to the customer.

***

## Login Route

The Medusa application defines an API route at `/auth/{actor_type}/{provider}` that authenticates a user of an actor type. It returns a JWT token that can be passed in [the header of subsequent requests](https://docs.medusajs.com/api/store#authentication) to send authenticated requests.

```bash
curl -X POST http://localhost:9000/auth/{actor_type}/{providers}
-H 'Content-Type: application/json' \
--data-raw '{
  "email": "Whitney_Schultz@gmail.com"
  // ...
}'
```

For example, if you're authenticating a customer, you send a request to `/auth/customer/emailpass`.

### Path Parameters

Its path parameters are:

- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`.
- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`.

### Request Body Parameters

This route accepts in the request body the data that the specified authentication provider requires to handle authentication.

For example, the EmailPass provider requires an `email` and `password` fields in the request body.

#### Overriding Callback URL

For the [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md) and [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) providers, you can pass a `callback_url` body parameter that overrides the `callbackUrl` set in the provider's configurations.

This is useful if you want to redirect the user to a different URL after authentication based on their actor type. For example, you can set different `callback_url` for admin users and customers.

### Response Fields

If the authentication is successful, you'll receive a `token` field in the response body object:

```json
{
  "token": "..."
}
```

Use that token in the header of subsequent requests to send authenticated requests.

If the authentication requires more action with a third-party service, you'll receive a `location` property:

```json
{
  "location": "https://..."
}
```

Redirect to that URL in the frontend to continue the authentication process with the third-party service.

[How to login Customers using the authentication route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/login/index.html.md).

***

## Validate Callback Route

The Medusa application defines an API route at `/auth/{actor_type}/{provider}/callback` that's useful for validating the authentication callback or redirect from third-party services like Google.

```bash
curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/callback?code=123&state=456
```

Refer to the [third-party authentication flow](#2-third-party-service-authenticate-flow) section to see how this route fits into the authentication flow.

### Path Parameters

Its path parameters are:

- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`.
- `{provider}`: the auth provider to handle the authentication. For example, `google`.

### Query Parameters

This route accepts all the query parameters that the third-party service sends to the frontend after the user completes the authentication process, such as the `code` and `state` query parameters.

### Response Fields

If the authentication is successful, you'll receive a `token` field in the response body object:

```json
{
  "token": "..."
}
```

In your frontend, decode the token using tools like [react-jwt](https://www.npmjs.com/package/react-jwt):

- If the decoded data has an `actor_id` property, the user is already registered. So, use this token for subsequent authenticated requests.
- If not, use the token in the header of a request that creates the user, such as the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers).

The decoded data may look like this:

```json
{
  "actor_id": "", // Empty if the user is not registered
  "user_metadata": {
    "email": "Whitney_Schultz@gmail.com"
  }
  // other fields...
}
```

***

## Refresh Token Route

The Medusa application defines an API route at `/auth/token/refresh` that's useful after authenticating a user with a third-party service to populate the user's token with their new information.

It requires the user's JWT token that they received from the authentication or callback routes.

```bash
curl -X POST http://localhost:9000/auth/token/refresh \
-H 'Authorization: Bearer {token}'
```

### Response Fields

If the token was refreshed successfully, you'll receive a `token` field in the response body object:

```json
{
  "token": "..."
}
```

Use that token in the header of subsequent requests to send authenticated requests.

***

## Reset Password Routes

To reset a user's password:

1. Generate a token using the [Generate Reset Password Token API route](#generate-reset-password-token-route).
   - The API route emits the `auth.password_reset` event, passing the token in the payload.
   - You can create a subscriber, as seen in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/reset-password/index.html.md), that listens to the event and send a notification to the user.
2. Pass the token to the [Reset Password API route](#reset-password-route) to reset the password.
   - The URL in the user's notification should direct them to a frontend URL, which sends a request to this route.

[Storefront Development: How to Reset a Customer's Password.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md)

### Generate Reset Password Token Route

The Medusa application defines an API route at `/auth/{actor_type}/{auth_provider}/reset-password` that emits the `auth.password_reset` event, passing the token in the payload.

```bash
curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/reset-password
-H 'Content-Type: application/json' \
--data-raw '{
  "identifier": "Whitney_Schultz@gmail.com"
}'
```

This API route is useful for providers like `emailpass` that store a user's password and use it for authentication.

#### Path Parameters

Its path parameters are:

- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`.
- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`.

#### Request Body Parameters

This route accepts in the request body an object having the following property:

- `identifier`: The user's identifier in the specified auth provider. For example, for the `emailpass` auth provider, you pass the user's email.

#### Response Fields

If the authentication is successful, the request returns a `201` response code.

### Reset Password Route

The Medusa application defines an API route at `/auth/{actor_type}/{auth_provider}/update` that accepts a token and, if valid, updates the user's password.

```bash
curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/update
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data-raw '{
  "email": "Whitney_Schultz@gmail.com",
  "password": "supersecret"
}'
```

This API route is useful for providers like `emailpass` that store a user's password and use it for logging them in.

#### Path Parameters

Its path parameters are:

- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`.
- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`.

#### Pass Token in Authorization Header

Before [Medusa v2.6](https://github.com/medusajs/medusa/releases/tag/v2.6), you passed the token as a query parameter. Now, you must pass it in the `Authorization` header.

In the request's authorization header, you must pass the token generated using the [Generate Reset Password Token route](#generate-reset-password-token-route). You pass it as a bearer token.

### Request Body Parameters

This route accepts in the request body an object that has the data necessary for the provider to update the user's password.

For the `emailpass` provider, you must pass the following properties:

- `email`: The user's email.
- `password`: The new password.

### Response Fields

If the authentication is successful, the request returns an object with a `success` property set to `true`:

```json
{
  "success": "true"
}
```


# How to Create an Actor Type

In this document, learn how to create an actor type and authenticate its associated data model.

## 0. Create Module with Data Model

Before creating an actor type, you must have a module with a data model representing the actor type.

Learn how to create a module in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).

The rest of this guide uses this `Manager` data model as an example:

```ts title="src/modules/manager/models/manager.ts"
import { model } from "@medusajs/framework/utils"

const Manager = model.define("manager", {
  id: model.id().primaryKey(),
  firstName: model.text(),
  lastName: model.text(),
  email: model.text(),
})

export default Manager
```

***

## 1. Create Workflow

Start by creating a workflow that does two things:

- Creates a record of the `Manager` data model.
- Sets the `app_metadata` property of the associated `AuthIdentity` record based on the new actor type.

For example, create the file `src/workflows/create-manager.ts`. with the following content:

```ts title="src/workflows/create-manager.ts" highlights={workflowHighlights}
import { 
  createWorkflow, 
  createStep,
  StepResponse,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  setAuthAppMetadataStep,
} from "@medusajs/medusa/core-flows"
import ManagerModuleService from "../modules/manager/service"

type CreateManagerWorkflowInput = {
  manager: {
    first_name: string
    last_name: string
    email: string
  }
  authIdentityId: string
}

const createManagerStep = createStep(
  "create-manager-step",
  async ({ 
    manager: managerData,
  }: Pick<CreateManagerWorkflowInput, "manager">, 
  { container }) => {
    const managerModuleService: ManagerModuleService = 
      container.resolve("manager")

    const manager = await managerModuleService.createManager(
      managerData
    )

    return new StepResponse(manager)
  }
)

const createManagerWorkflow = createWorkflow(
  "create-manager",
  function (input: CreateManagerWorkflowInput) {
    const manager = createManagerStep({
      manager: input.manager,
    })

    setAuthAppMetadataStep({
      authIdentityId: input.authIdentityId,
      actorType: "manager",
      value: manager.id,
    })

    return new WorkflowResponse(manager)
  }
)

export default createManagerWorkflow
```

This workflow accepts the manager’s data and the associated auth identity’s ID as inputs. The next sections explain how the auth identity ID is retrieved.

The workflow has two steps:

1. Create the manager using the `createManagerStep`.
2. Set the `app_metadata` property of the associated auth identity using the `setAuthAppMetadataStep` from Medusa's core workflows. You specify the actor type `manager` in the `actorType` property of the step’s input.

***

## 2. Define the Create API Route

Next, you’ll use the workflow defined in the previous section in an API route that creates a manager.

So, create the file `src/api/manager/route.ts` with the following content:

```ts title="src/api/manager/route.ts" highlights={createRouteHighlights}
import type { 
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
import createManagerWorkflow from "../../workflows/create-manager"

type RequestBody = {
  first_name: string
  last_name: string
  email: string
}

export async function POST(
  req: AuthenticatedMedusaRequest<RequestBody>, 
  res: MedusaResponse
) {
  // If `actor_id` is present, the request carries 
  // authentication for an existing manager
  if (req.auth_context.actor_id) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "Request already authenticated as a manager."
    )
  }

  const { result } = await createManagerWorkflow(req.scope)
    .run({
      input: {
        manager: req.body,
        authIdentityId: req.auth_context.auth_identity_id,
      },
    })
  
    res.status(200).json({ manager: result })
}
```

Since the manager must be associated with an `AuthIdentity` record, the request is expected to be authenticated, even if the manager isn’t created yet. This can be achieved by:

1. Obtaining a token using the [/auth route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md).
2. Passing the token in the bearer header of the request to this route.

In the API route, you create the manager using the workflow from the previous section and return it in the response.

***

## 3. Apply the `authenticate` Middleware

The last step is to apply the `authenticate` middleware on the API routes that require a manager’s authentication.

To do that, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" highlights={middlewareHighlights}
import { 
  defineMiddlewares,
  authenticate,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/manager",
      method: "POST",
      middlewares: [
        authenticate("manager", ["session", "bearer"], {
          allowUnregistered: true,
        }),
      ],
    },
    {
      matcher: "/manager/me*",
      middlewares: [
        authenticate("manager", ["session", "bearer"]),
      ],
    },
  ],
})
```

This applies middlewares on two route patterns:

1. The `authenticate` middleware is applied on the `/manager` API route for `POST` requests while allowing unregistered managers. This requires that a bearer token be passed in the request to access the manager’s auth identity but doesn’t require the manager to be registered.
2. The `authenticate` middleware is applied on all routes starting with `/manager/me`, restricting these routes to authenticated managers only.

### Retrieve Manager API Route

For example, create the file `src/api/manager/me/route.ts` with the following content:

```ts title="src/api/manager/me/route.ts"
import { 
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import ManagerModuleService from "../../../modules/manager/service"

export async function GET(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
): Promise<void> {
  const query = req.scope.resolve("query")
  const managerId = req.auth_context?.actor_id

  const { data: [manager] } = await query.graph({
    entity: "manager",
    fields: ["*"],
    filters: {
      id: managerId,
    },
  }, {
    throwIfKeyNotFound: true,
  })

  res.json({ manager })
}
```

This route is only accessible by authenticated managers. You access the manager’s ID using `req.auth_context.actor_id`.

***

## Test Custom Actor Type Authentication Flow

To authenticate managers:

1. Send a `POST` request to `/auth/manager/emailpass/register` to create an auth identity for the manager:

```bash
curl -X POST 'http://localhost:9000/auth/manager/emailpass/register' \
-H 'Content-Type: application/json' \
--data-raw '{
    "email": "manager@gmail.com",
    "password": "supersecret"
}'
```

Copy the returned token to use it in the next request.

2. Send a `POST` request to `/manager` to create a manager:

```bash
curl -X POST 'http://localhost:9000/manager' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data-raw '{
    "first_name": "John",
    "last_name": "Doe",
    "email": "manager@gmail.com"
}'
```

Replace `{token}` with the token returned in the previous step.

3. Send a `POST` request to `/auth/manager/emailpass` again to retrieve an authenticated token for the manager:

```bash
curl -X POST 'http://localhost:9000/auth/manager/emailpass' \
-H 'Content-Type: application/json' \
--data-raw '{
    "email": "manager@gmail.com",
    "password": "supersecret"
}'
```

4. You can now send authenticated requests as a manager. For example, send a `GET` request to `/manager/me` to retrieve the authenticated manager’s details:

```bash
curl 'http://localhost:9000/manager/me' \
-H 'Authorization: Bearer {token}'
```

Whenever you want to log in as a manager, use the `/auth/manager/emailpass` API route, as explained in step 3.

***

## Delete User of Actor Type

When you delete a user of the actor type, you must update its auth identity to remove the association to the user.

For example, create the following workflow that deletes a manager and updates its auth identity, create the file `src/workflows/delete-manager.ts` with the following content:

```ts title="src/workflows/delete-manager.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import ManagerModuleService from "../modules/manager/service"

export type DeleteManagerWorkflow = {
  id: string
}

const deleteManagerStep = createStep(
  "delete-manager-step",
  async (
    { id }: DeleteManagerWorkflow, 
    { container }) => {
      const managerModuleService: ManagerModuleService = 
        container.resolve("manager")

      const manager = await managerModuleService.retrieve(id)

      await managerModuleService.deleteManagers(id)

      return new StepResponse(undefined, { manager })
    },
    async ({ manager }, { container }) => {
      const managerModuleService: ManagerModuleService = 
        container.resolve("manager")

      await managerModuleService.createManagers(manager)
    }
  )
```

You add a step that deletes the manager using the `deleteManagers` method of the module's main service. In the compensation function, you create the manager again.

Next, in the same file, add the workflow that deletes a manager:

```ts title="src/workflows/delete-manager.ts" collapsibleLines="1-15" expandButtonLabel="Show Imports" highlights={deleteHighlights}
// other imports
import { MedusaError } from "@medusajs/framework/utils"
import {
  WorkflowData,
  WorkflowResponse,
  createWorkflow,
  transform,
} from "@medusajs/framework/workflows-sdk"
import { 
  setAuthAppMetadataStep,
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"

// ...

export const deleteManagerWorkflow = createWorkflow(
  "delete-manager",
  (
    input: WorkflowData<DeleteManagerWorkflow>
  ): WorkflowResponse<string> => {
    deleteManagerStep(input)

    const { data: authIdentities } = useQueryGraphStep({
      entity: "auth_identity",
      fields: ["id"],
      filters: {
        app_metadata: {
          // the ID is of the format `{actor_type}_id`.
          manager_id: input.id,
        },
      },
    })

    const authIdentity = transform(
      { authIdentities },
      ({ authIdentities }) => {
        const authIdentity = authIdentities[0]

        if (!authIdentity) {
          throw new MedusaError(
            MedusaError.Types.NOT_FOUND,
            "Auth identity not found"
          )
        }

        return authIdentity
      }
    )

    setAuthAppMetadataStep({
      authIdentityId: authIdentity.id,
      actorType: "manager",
      value: null,
    })

    return new WorkflowResponse(input.id)
  }
)
```

In the workflow, you:

1. Use the `deleteManagerStep` defined earlier to delete the manager.
2. Retrieve the auth identity of the manager using Query. To do that, you filter the `app_metadata` property of an auth identity, which holds the user's ID under `{actor_type_name}_id`. So, in this case, it's `manager_id`.
3. Check that the auth identity exist, then, update the auth identity to remove the ID of the manager from it.

You can use this workflow when deleting a manager, such as in an API route.


# Auth Module Options

In this document, you'll learn about the options of the Auth Module.

## providers

The `providers` option is an array of auth module providers.

When the Medusa application starts, these providers are registered and can be used to handle authentication.

By default, the `emailpass` provider is registered to authenticate customers and admin users.

For example:

```ts title="medusa-config.ts"
import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/auth",
      dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER],
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/auth-emailpass",
            id: "emailpass",
            options: {
              // provider options...
            },
          },
        ],
      },
    },
  ],
})
```

The `providers` option is an array of objects that accept the following properties:

- `resolve`: A string indicating the package name of the module provider or the path to it relative to the `src` directory.
- `id`: A string indicating the provider's unique name or ID.
- `options`: An optional object of the module provider's options.

***

## Auth CORS

The Medusa application's authentication API routes are defined under the `/auth` prefix that requires setting the `authCors` property of the `http` configuration.

By default, the Medusa application you created will have an `AUTH_CORS` environment variable, which is used as the value of `authCors`.

Refer to [Medusa's configuration guide](https://docs.medusajs.com/docs/learn/configurations/medusa-config#httpauthCors/index.html.md) to learn more about the `authCors` configuration.

***

## authMethodsPerActor Configuration

The Medusa application's configuration accept an `authMethodsPerActor` configuration which restricts the allowed auth providers used with an actor type.

Learn more about the `authMethodsPerActor` configuration in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers#configure-allowed-auth-providers-of-actor-types/index.html.md).


# Auth Module

In this section of the documentation, you will find resources to learn more about the Auth Module and how to use it in your application.

Medusa has auth related features available out-of-the-box through the Auth Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Auth Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Auth Features

- [Basic User Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#1-basic-authentication-flow/index.html.md): Authenticate users using their email and password credentials.
- [Third-Party and Social Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#2-third-party-service-authenticate-flow/index.html.md): Authenticate users using third-party services and social platforms, such as [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) and [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md).
- [Authenticate Custom Actor Types](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md): Create custom user or actor types, such as managers, authenticate them in your application, and guard routes based on the custom user types.
- [Custom Authentication Providers](https://docs.medusajs.com/references/auth/provider/index.html.md): Integrate third-party services with custom authentication providers.

***

## How to Use the Auth Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/authenticate-user.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules, MedusaError } from "@medusajs/framework/utils"
import { MedusaRequest } from "@medusajs/framework/http"
import { AuthenticationInput } from "@medusajs/framework/types"

type Input = {
  req: MedusaRequest
}

const authenticateUserStep = createStep(
  "authenticate-user",
  async ({ req }: Input, { container }) => {
    const authModuleService = container.resolve(Modules.AUTH)

    const { success, authIdentity, error } = await authModuleService
      .authenticate(
        "emailpass",
       {
          url: req.url,
          headers: req.headers,
          query: req.query,
          body: req.body,
          authScope: "admin", // or custom actor type
          protocol: req.protocol,
        } as AuthenticationInput
      )

    if (!success) {
      // incorrect authentication details
      throw new MedusaError(
        MedusaError.Types.UNAUTHORIZED,
        error || "Incorrect authentication details"
      )
    }

    return new StepResponse({ authIdentity }, authIdentity?.id)
  },
  async (authIdentityId, { container }) => {
    if (!authIdentityId) {
      return
    }
    
    const authModuleService = container.resolve(Modules.AUTH)

    await authModuleService.deleteAuthIdentities([authIdentityId])
  }
)

export const authenticateUserWorkflow = createWorkflow(
  "authenticate-user",
  (input: Input) => {
    const { authIdentity } = authenticateUserStep(input)

    return new WorkflowResponse({
      authIdentity,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

```ts title="API Route" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { authenticateUserWorkflow } from "../../workflows/authenticate-user"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await authenticateUserWorkflow(req.scope)
    .run({
      req,
    })

  res.send(result)
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***

## Configure Auth Module

The Auth Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/module-options/index.html.md) for details on the module's options.

***

## Providers

Medusa provides the following authentication providers out-of-the-box. You can use them to authenticate admin users, customers, or custom actor types.

***


# Send Reset Password Email Notification

In this guide, you'll learn how to handle the `auth.password_reset` event to send a reset password email (or other notification type) to users.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/reset-password/index.html.md) to learn how to reset your user admin password using the dashboard.

## Reset Password Flow Overview

![Diagram showcasing the reset password flow detailed below](https://res.cloudinary.com/dza7lstvk/image/upload/v1754050032/Medusa%20Resources/reset-password_qheixj.jpg)

Users of any actor type (admin, customer, or custom actor type) can request to reset their password. The flow for resetting a password is as follows:

1. The user requests to reset their password either through the frontend (for example, [Medusa Admin](https://docs.medusajs.com/user-guide/reset-password/index.html.md)) or the [Generate Reset Password Token API route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#generate-reset-password-token-route/index.html.md).
2. The Medusa application generates a password reset token and emits the `auth.password_reset` event.
   - At this point, you can handle the event to send a notification to the user with instructions on how to reset their password.
3. The user receives the notification and clicks on the link to reset their password.
   - The user can reset their password either through the frontend (for example, [Medusa Admin](https://docs.medusajs.com/user-guide/reset-password/index.html.md)) or the [Reset Password API route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#reset-password-route/index.html.md).

In this guide, you'll implement a subscriber that handles the `auth.password_reset` event to send an email notification to the user with instructions on how to reset their password.

After adding the subscriber, you will have a complete reset password flow you can utilize using the Medusa Admin, storefront, or API routes.

***

## Prerequisites: Notification Module Provider

To send an email or notification to the user, you must have a Notification Module Provider set up.

Medusa provides providers like [SendGrid](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md) and [Resend](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/integrations/guides/resend/index.html.md), and you can also [create your own custom provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md).

Refer to the [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification#what-is-a-notification-module-provider/index.html.md) documentation for a list of available providers and how to set them up.

### Testing with the Local Notification Module Provider

For testing purposes, you can use the [Local Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/local/index.html.md) by adding this to your `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          // ...
          {
            resolve: "@medusajs/medusa/notification-local",
            id: "local",
            options: {
              channels: ["email"],
            },
          },
        ],
      },
    },
  ],
})
```

The Local provider logs email details to your terminal instead of sending actual emails, which is useful for development and testing.

***

## Create the Reset Password Subscriber

To create a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that handles the `auth.password_reset` event, create the file `src/subscribers/password-reset.ts` with the following content:

```ts title="src/subscribers/handle-reset.ts" highlights={highlights} collapsibleLines="1-6" expandMoreLabel="Show Imports"
import {
  SubscriberArgs,
  type SubscriberConfig,
} from "@medusajs/medusa"
import { Modules } from "@medusajs/framework/utils"

export default async function resetPasswordTokenHandler({
  event: { data: {
    entity_id: email,
    token,
    actor_type,
  } },
  container,
}: SubscriberArgs<{ entity_id: string, token: string, actor_type: string }>) {
  const notificationModuleService = container.resolve(
    Modules.NOTIFICATION
  )
  const config = container.resolve("configModule")

  let urlPrefix = ""

  if (actor_type === "customer") {
    urlPrefix = config.admin.storefrontUrl || "https://storefront.com"
  } else {
    const backendUrl = config.admin.backendUrl !== "/" ? config.admin.backendUrl :
      "http://localhost:9000"
    const adminPath = config.admin.path
    urlPrefix = `${backendUrl}${adminPath}`
  }

  await notificationModuleService.createNotifications({
    to: email,
    channel: "email",
    // TODO replace with template ID in notification provider
    template: "password-reset",
    data: {
      // a URL to a frontend application
      reset_url: `${urlPrefix}/reset-password?token=${token}&email=${email}`,
    },
  })
}

export const config: SubscriberConfig = {
  event: "auth.password_reset",
}
```

The subscriber receives the following data through the event payload:

- `entity_id`: The identifier of the user. When using the `emailpass` provider, it's the user's email.
- `token`: The token to reset the user's password.
- `actor_type`: The user's actor type. For example, if the user is a customer, the `actor_type` is `customer`. If it's an admin user, the `actor_type` is `user`.

This event's payload previously had an `actorType` field. It was renamed to `actor_type` after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7).

### Reset Password URL

Based on the user's actor type, you set the URL prefix to redirect the user to the appropriate frontend page to reset their password:

- If the user is a customer, you set the URL prefix to the storefront URL.
- If the user is an admin, you set the URL prefix to the backend URL, which is constructed from the `config.admin.backendUrl` and `config.admin.path` values.

Note that the Medusa Admin has a reset password form at `/reset-password?token={token}&email={email}`.

### Notification Configurations

For the notification, you can configure the following fields:

- `to`: The identifier to send the notification to, which in this case is the email.
- `channel`: The channel to send the notification through, which in this case is email.
- `template`: The template ID of the email to send. This ID depends on the Notification Module provider you use. For example, if you use SendGrid, this would be the ID of the SendGrid template.
  - Refer to the [Example Notification Templates](#example-notification-templates) section below for examples of notification templates to use.
- `data`: The data payload to pass to the template. You can pass additional fields, if necessary.

### Test It Out

After you set up the Notification Module Provider, create a template in the provider, and create the subscriber, you can test the reset password flow.

Start the Medusa application with the following command:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin (locally at `http://localhost:9000/app`) and click on "Reset" in the login form. Then, enter the email of the user you want to reset the password for.

Once you reset the password, you should see that the `auth.password_reset` event is emitted in the server's logs:

```bash
info:    Processing auth.password_reset which has 1 subscribers
```

If you're using an email Notification Module Provider, check the user's email inbox for the reset password email with the link to reset their password.

If you're using the Local provider, check your terminal for the logged email details.

***

## Next Steps: Implementing Frontend

In your frontend, you must have a page that accepts `token` and `email` query parameters.

The page shows the user password fields to enter their new password, then submits the new password, token, and email to the [Reset Password Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#reset-password-route/index.html.md).

The Medusa Admin already has a reset password page at `/reset-password?token={token}&email={email}`. So, you only need to implement this page in your storefront or custom admin dashboard.

### Examples

- [Storefront Guide: Reset Customer Password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md)

***

## Example Notification Templates

The following section provides example notification templates for some Notification Module Providers.

### SendGrid

Refer to the [SendGrid Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md) documentation for more details on how to set up SendGrid.

The following HTML template can be used with SendGrid to send a reset password email:

```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Reset Your Password</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
            background-color: #ffffff;
            margin: 0;
            padding: 20px;
        }
        .container {
            max-width: 465px;
            margin: 40px auto;
            border: 1px solid #eaeaea;
            border-radius: 5px;
            padding: 20px;
        }
        .header {
            text-align: center;
            margin: 30px 0;
        }
        .title {
            color: #000000;
            font-size: 24px;
            font-weight: normal;
            margin: 0;
        }
        .content {
            margin: 32px 0;
        }
        .text {
            color: #000000;
            font-size: 14px;
            line-height: 24px;
            margin: 0 0 16px 0;
        }
        .button-container {
            text-align: center;
            margin: 32px 0;
        }
        .reset-button {
            background-color: #000000;
            border-radius: 3px;
            color: #ffffff;
            font-size: 12px;
            font-weight: 600;
            text-decoration: none;
            text-align: center;
            padding: 12px 20px;
            display: inline-block;
        }
        .reset-button:hover {
            background-color: #333333;
        }
        .url-section {
            margin: 32px 0;
        }
        .url-link {
            color: #2563eb;
            text-decoration: none;
            font-size: 14px;
            line-height: 24px;
            word-break: break-all;
        }
        .disclaimer {
            margin: 32px 0;
        }
        .disclaimer-text {
            color: #666666;
            font-size: 12px;
            line-height: 24px;
            margin: 0 0 8px 0;
        }
        .security-footer {
            margin-top: 32px;
            padding-top: 20px;
            border-top: 1px solid #eaeaea;
        }
        .security-text {
            color: #666666;
            font-size: 12px;
            line-height: 24px;
            margin: 0;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1 class="title">Reset Your Password</h1>
        </div>

        <div class="content">
            <p class="text">
                Hello{{#if email}} {{email}}{{/if}},
            </p>
            <p class="text">
                We received a request to reset your password. Click the button below to create a new password for your account.
            </p>
        </div>

        <div class="button-container">
            <a href="{{reset_url}}" class="reset-button">
                Reset Password
            </a>
        </div>

        <div class="url-section">
            <p class="text">
                Or copy and paste this URL into your browser:
            </p>
            <a href="{{reset_url}}" class="url-link">
                {{reset_url}}
            </a>
        </div>

        <div class="disclaimer">
            <p class="disclaimer-text">
                This password reset link will expire soon for security reasons.
            </p>
            <p class="disclaimer-text">
                If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged.
            </p>
        </div>

        <div class="security-footer">
            <p class="security-text">
                For security reasons, never share this reset link with anyone. If you're having trouble with the button above, copy and paste the URL into your web browser.
            </p>
        </div>
    </div>
</body>
</html>
```

Make sure to pass the `reset_url` variable to the template, which contains the URL to reset the password.

You can also customize the template further to show other information.

### Resend

If you've integrated Resend as explained in the [Resend Integration Guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/integrations/guides/resend/index.html.md), you can add a new template for password reset emails at `src/modules/resend/emails/password-reset.tsx`:

```tsx title="src/modules/resend/emails/password-reset.tsx"
import { 
  Text, 
  Container, 
  Heading, 
  Html, 
  Section, 
  Tailwind, 
  Head, 
  Preview, 
  Body, 
  Link,
  Button, 
} from "@react-email/components"

type PasswordResetEmailProps = {
  reset_url: string
  email?: string
}

function PasswordResetEmailComponent({ reset_url, email }: PasswordResetEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Reset your password</Preview>
      <Tailwind>
        <Body className="bg-white my-auto mx-auto font-sans px-2">
          <Container className="border border-solid border-[#eaeaea] rounded my-[40px] mx-auto p-[20px] max-w-[465px]">
            <Section className="mt-[32px]">
              <Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
                Reset Your Password
              </Heading>
            </Section>

            <Section className="my-[32px]">
              <Text className="text-black text-[14px] leading-[24px]">
                Hello{email ? ` ${email}` : ""},
              </Text>
              <Text className="text-black text-[14px] leading-[24px]">
                We received a request to reset your password. Click the button below to create a new password for your account.
              </Text>
            </Section>

            <Section className="text-center mt-[32px] mb-[32px]">
              <Button
                className="bg-[#000000] rounded text-white text-[12px] font-semibold no-underline text-center px-5 py-3"
                href={reset_url}
              >
                Reset Password
              </Button>
            </Section>

            <Section className="my-[32px]">
              <Text className="text-black text-[14px] leading-[24px]">
                Or copy and paste this URL into your browser:
              </Text>
              <Link
                href={reset_url}
                className="text-blue-600 no-underline text-[14px] leading-[24px] break-all"
              >
                {reset_url}
              </Link>
            </Section>

            <Section className="my-[32px]">
              <Text className="text-[#666666] text-[12px] leading-[24px]">
                This password reset link will expire soon for security reasons.
              </Text>
              <Text className="text-[#666666] text-[12px] leading-[24px] mt-2">
                If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged.
              </Text>
            </Section>

            <Section className="mt-[32px] pt-[20px] border-t border-solid border-[#eaeaea]">
              <Text className="text-[#666666] text-[12px] leading-[24px]">
                For security reasons, never share this reset link with anyone. If you're having trouble with the button above, copy and paste the URL into your web browser.
              </Text>
            </Section>
          </Container>
        </Body>
      </Tailwind>
    </Html>
  )
}

export const passwordResetEmail = (props: PasswordResetEmailProps) => (
  <PasswordResetEmailComponent {...props} />
)

// Mock data for preview/development
const mockPasswordReset: PasswordResetEmailProps = {
  reset_url: "https://your-app.com/reset-password?token=sample-reset-token-123",
  email: "user@example.com",
}

export default () => <PasswordResetEmailComponent {...mockPasswordReset} />
```

Feel free to customize the email template further to match your branding and style, or to add additional information.

Then, in the Resend Module's service at `src/modules/resend/service.ts`, add the new template to the `templates` object and `Templates` type:

```ts title="src/modules/resend/service.ts"
// other imports...
import { passwordResetEmail } from "./emails/password-reset"

enum Templates {
  // ...
  PASSWORD_RESET = "password-reset",
}

const templates: {[key in Templates]?: (props: unknown) => React.ReactNode} = {
  // ...
  [Templates.PASSWORD_RESET]: passwordResetEmail,
}
```

Finally, find the `getTemplateSubject` function in the `ResendNotificationProviderService` and add a case for the `USER_INVITED` template:

```ts title="src/modules/resend/service.ts"
class ResendNotificationProviderService extends AbstractNotificationProviderService {
  // ...

  private getTemplateSubject(template: Templates) {
    // ...
    switch (template) {
      // ...
      case Templates.PASSWORD_RESET:
        return "Reset Your Password"
    }
  }
}
```


# Retrieve Cart Totals using Query

In this guide, you'll learn how to retrieve cart totals in your Medusa application using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).

You may need to retrieve cart totals in your Medusa customizations, such as workflows or custom API routes, to perform custom actions with them. The ideal way to retrieve totals is with Query.

Refer to the [Retrieve Cart Totals in Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/cart/totals/index.html.md) guide for a storefront-specific approach.

## How to Retrieve Cart Totals with Query

To retrieve cart totals, you mainly need to pass the `total` field within the `fields` option of the Query. This will return the cart's grand total, along with the totals of its line items and shipping methods. You can also pass additional total fields that you need for your use case.

For example, to retrieve all totals of a cart:

Use `useQueryGraphStep` in workflows, and `query.graph` in custom API routes, scheduled jobs, and subscribers.

### useQueryGraphStep

```ts highlights={[["12"]]}
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

export const myWorkflow = createWorkflow(
  "my-workflow",
  () => {
    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      fields: [
        "id",
        "currency_code",
        "total",
        "subtotal",
        "tax_total",
        "discount_total",
        "discount_subtotal",
        "discount_tax_total",
        "original_total",
        "original_tax_total",
        "item_total",
        "item_subtotal",
        "item_tax_total",
        "original_item_total",
        "original_item_subtotal",
        "original_item_tax_total",
        "shipping_total",
        "shipping_subtotal",
        "shipping_tax_total",
        "original_shipping_tax_total",
        "original_shipping_subtotal",
        "original_shipping_total",
        "credit_line_subtotal",
        "credit_line_tax_total",
        "credit_line_total",
        "items.*",
        "shipping_methods.*",
      ],
      filters: {
        id: "cart_123", // Specify the cart ID
      },
    })
  }
)
```

### query.graph

```ts highlights={[["8"]]}
const query = container.resolve("query") // or req.scope.resolve in API routes

const { data: [cart] } = await query.graph({
  entity: "cart",
  fields: [
    "id",
    "currency_code",
    "total",
    "subtotal",
    "tax_total",
    "discount_total",
    "discount_subtotal",
    "discount_tax_total",
    "original_total",
    "original_tax_total",
    "item_total",
    "item_subtotal",
    "item_tax_total",
    "original_item_total",
    "original_item_subtotal",
    "original_item_tax_total",
    "shipping_total",
    "shipping_subtotal",
    "shipping_tax_total",
    "original_shipping_tax_total",
    "original_shipping_subtotal",
    "original_shipping_total",
    "credit_line_subtotal",
    "credit_line_tax_total",
    "credit_line_total",
    "items.*",
    "shipping_methods.*",
  ],
  filters: {
    id: "cart_123", // Specify the cart ID
  },
})
```

The returned `cart` object will look like this:

```json
{
  "id": "cart_123",
  "currency_code": "usd",
  "total": 10,
  "subtotal": 10,
  "tax_total": 0,
  "discount_total": 0,
  "discount_subtotal": 0,
  "discount_tax_total": 0,
  "original_total": 10,
  "original_tax_total": 0,
  "item_total": 10,
  "item_subtotal": 10,
  "item_tax_total": 0,
  "original_item_total": 10,
  "original_item_subtotal": 10,
  "original_item_tax_total": 0,
  "shipping_total": 10,
  "shipping_subtotal": 10,
  "shipping_tax_total": 0,
  "original_shipping_tax_total": 0,
  "original_shipping_subtotal": 10,
  "original_shipping_total": 10,
  "credit_line_subtotal": 0,
  "credit_line_tax_total": 0,
  "credit_line_total": 0,
  "items": [
    {
      "id": "cali_123",
      // ...
      "unit_price": 10,
      "subtotal": 10,
      "total": 0,
      "original_total": 10,
      "discount_total": 0,
      "discount_subtotal": 0,
      "discount_tax_total": 0,
      "tax_total": 0,
      "original_tax_total": 0,
    }
  ],
  "shipping_methods": [
    {
      "id": "casm_01K10AYZDKZGQXE8WXW3QP9T22",
      // ...
      "amount": 10,
      "subtotal": 10,
      "total": 10,
      "original_total": 10,
      "discount_total": 0,
      "discount_subtotal": 0,
      "discount_tax_total": 0,
      "tax_total": 0,
      "original_tax_total": 0,
    }
  ]
}
```

### Cart Totals

The cart will include the following total fields:

- `total`: The cart's final total after discounts and credit lines, including taxes.
- `subtotal`: The cart's subtotal before discounts, excluding taxes. Calculated as the sum of `item_subtotal` and `shipping_subtotal`.
- `tax_total`: The cart's tax total after discounts. Calculated as the sum of `item_tax_total` and `shipping_tax_total`.
- `discount_total`: The total amount of discounts applied to the cart, including the tax portion of discounts.
- `discount_subtotal`: The total amount of discounts applied to the cart's subtotal (excluding tax portion). Used in the final total calculation.
- `discount_tax_total`: The total amount of discounts applied to the cart's tax. Represents the tax portion of discounts.
- `original_total`: The cart's total before discounts, including taxes. Calculated as the sum of `original_item_total` and `original_shipping_total`.
- `original_tax_total`: The cart's tax total before discounts. Calculated as the sum of `original_item_tax_total` and `original_shipping_tax_total`.
- `item_total`: The sum of all line items' totals after discounts, including taxes.
- `item_subtotal`: The sum of all line items' subtotals before discounts, excluding taxes.
- `item_tax_total`: The sum of all line items' tax totals after discounts.
- `original_item_total`: The sum of all line items' original totals before discounts, including taxes.
- `original_item_subtotal`: The sum of all line items' original subtotals before discounts, excluding taxes.
- `original_item_tax_total`: The sum of all line items' original tax totals before discounts.
- `shipping_total`: The sum of all shipping methods' totals after discounts, including taxes.
- `shipping_subtotal`: The sum of all shipping methods' subtotals before discounts, excluding taxes.
- `shipping_tax_total`: The sum of all shipping methods' tax totals after discounts.
- `original_shipping_tax_total`: The sum of all shipping methods' original tax totals before discounts.
- `original_shipping_subtotal`: The sum of all shipping methods' original subtotals before discounts, excluding taxes.
- `original_shipping_total`: The sum of all shipping methods' original totals before discounts, including taxes.
- `credit_line_subtotal`: The subtotal of credit lines applied to the cart, excluding taxes.
- `credit_line_tax_total`: The tax total of credit lines applied to the cart.
- `credit_line_total`: The total amount of credit lines applied to the cart, including taxes. Subtracted from the final total.

### Cart Line Item Totals

The `items` array in the `cart` object contains total fields for each line item:

- `unit_price`: The price of a single unit of the line item. This field is not calculated and is stored in the database.
- `subtotal`: The line item's subtotal before discounts, excluding taxes.
- `total`: The line item's total after discounts, including taxes.
- `original_total`: The line item's original total before discounts, including taxes.
- `discount_total`: The total amount of discounts applied to the line item, including the tax portion of discounts.
- `discount_subtotal`: The total amount of discounts applied to the line item's subtotal (excluding tax portion).
- `discount_tax_total`: The total amount of discounts applied to the line item's tax. Represents the tax portion of discounts.
- `tax_total`: The line item's tax total after discounts.
- `original_tax_total`: The line item's original tax total before discounts.

### Cart Shipping Method Totals

The `shipping_methods` array in the `cart` object contains total fields for each shipping method:

- `amount`: The amount charged for the shipping method. This field is not calculated and is stored in the database.
- `subtotal`: The shipping method's subtotal before discounts, excluding taxes.
- `total`: The shipping method's total after discounts, including taxes.
- `original_total`: The shipping method's original total before discounts, including taxes.
- `discount_total`: The total amount of discounts applied to the shipping method, including the tax portion of discounts.
- `discount_subtotal`: The total amount of discounts applied to the shipping method's subtotal (excluding tax portion).
- `discount_tax_total`: The total amount of discounts applied to the shipping method's tax. Represents the tax portion of discounts.
- `tax_total`: The shipping method's tax total after discounts.
- `original_tax_total`: The shipping method's original tax total before discounts.

***

## Caveats of Retrieving Cart Totals

### Using Asterisk (\*) in Cart's Query

Cart totals are calculated based on the cart's line items, shipping methods, taxes, and any discounts applied. They are not stored in the `Cart` data model.

For that reason, you cannot retrieve cart totals by passing `*` to the Query's fields. For example, the following query will not return cart totals:

### useQueryGraphStep

```ts
const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: ["*"],
  filters: {
    id: "cart_123",
  },
})

// carts don't include cart totals
```

### query.graph

```ts
const { data: [cart] } = await query.graph({
  entity: "cart",
  fields: ["*"],
  filters: {
    id: "cart_123",
  },
})

// cart doesn't include cart totals
```

This will return the cart data stored in the database, but not the calculated totals.

You also can't pass `*` along with `total` in the Query's fields option. Passing `*` will override the `total` field, and you will not retrieve cart totals. For example, the following query will not return cart totals:

### useQueryGraphStep

```ts
const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: ["*", "total"],
  filters: {
    id: "cart_123",
  },
})

// carts don't include cart totals
```

### query.graph

```ts
const { data: [cart] } = await query.graph({
  entity: "cart",
  fields: ["*", "total"],
  filters: {
    id: "cart_123",
  },
})

// cart doesn't include cart totals
```

Instead, when you need to retrieve cart totals, explicitly pass the `total` field in the Query's fields option, as shown in [the previous section](#how-to-retrieve-cart-totals-with-query). You can also include other fields and relations you need, such as `email` and `items.*`.

### Applying Filters on Cart Totals

You can't apply filters directly on the cart totals, as they are not stored in the `Cart` data model. You can still filter the cart based on its properties, such as `id`, `email`, or `currency_code`, but not on the calculated totals.


# Cart Concepts

In this document, you’ll learn about the main concepts related to carts in Medusa.

## Cart

A cart is the selection of product variants that a customer intends to purchase. It is represented by the [Cart data model](https://docs.medusajs.com/references/cart/models/Cart/index.html.md).

A cart holds information about:

- The items the customer wants to buy.
- The customer's shipping and billing addresses.
- The shipping methods used to fulfill the items after purchase.
- The payment method and information necessary to complete the purchase.
  - These are stored and handled by the [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md).

### Cart Locale

Cart locale is available starting [Medusa v2.12.3](https://github.com/medusajs/medusa/releases/tag/v2.12.3).

The `Cart` data model has a `locale` property that indicates the locale of the cart. This locale is in the [IETF BCP 47 standard](https://gist.github.com/typpo/b2b828a35e683b9bf8db91b5404f1bd1) format, such as `en-US` for American English or `fr-FR` for French (France).

When creating a cart, you can set the `locale` property to specify the desired locale for the cart. The information of the items in the cart, such as product titles and descriptions, will be displayed in the specified locale if translations are available.

Refer to the [Translation Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/translation/index.html.md) to learn more about how translations and locales work.

***

## Line Items

A line item, represented by the [LineItem](https://docs.medusajs.com/references/cart/models/LineItem/index.html.md) data model, is a quantity of a product variant added to the cart. A cart has multiple line items.

In the Medusa application, a product variant is implemented in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md).

A line item stores some of the product variant’s properties, such as the `product_title` and `product_description`. It also stores data related to the item’s quantity and price.

***

## Shipping and Billing Addresses

A cart has a shipping and billing address. Both of these addresses are represented by the [Address data model](https://docs.medusajs.com/references/cart/models/Address/index.html.md).

![A diagram showcasing the relation between the Cart and Address data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711532392/Medusa%20Resources/cart-addresses_ls6qmv.jpg)

***

## Shipping Methods

A shipping method, represented by the [ShippingMethod data model](https://docs.medusajs.com/references/cart/models/ShippingMethod/index.html.md), is used to fulfill the items in the cart after the order is placed. A cart can have more than one shipping method.

In the Medusa application, the shipping method is created from a shipping option, available through the [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md). Its ID is stored in the `shipping_option_id` property of the method.

### data Property

After an order is placed, you can use a third-party fulfillment provider to fulfill its shipments.

If the fulfillment provider requires additional custom data to be passed along from the checkout process, set this data in the `ShippingMethod`'s `data` property.

The `data` property is an object used to store custom data relevant later for fulfillment.


# Links between Cart Module and Other Modules

This document showcases the module links defined between the Cart Module and other Commerce Modules.

## Summary

The Cart Module has the following links to other modules:

Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|Cart|Customer|Read-only - has one|Learn more|
|ShippingMethod|ShippingOption|Read-only - has one|Learn more|
|Order|Cart|Stored - one-to-one|Learn more|
|Cart|PaymentCollection|Stored - one-to-one|Learn more|
|LineItem|Product|Read-only - has one|Learn more|
|LineItem|ProductVariant|Read-only - has one|Learn more|
|Cart|Promotion|Stored - many-to-many|Learn more|
|Cart|Region|Read-only - has one|Learn more|
|Cart|SalesChannel|Read-only - has one|Learn more|

***

## Customer Module

Medusa defines a read-only link between the `Cart` data model and the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md)'s `Customer` data model. This means you can retrieve the details of a cart's customer, but you don't manage the links in a pivot table in the database. The customer of a cart is determined by the `customer_id` property of the `Cart` data model.

### Retrieve with Query

To retrieve the customer of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`:

### query.graph

```ts
const { data: carts } = await query.graph({
  entity: "cart",
  fields: [
    "customer.*",
  ],
})

// carts[0].customer
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: [
    "customer.*",
  ],
})

// carts[0].customer
```

***

## Fulfillment Module

Medusa defines a read-only link between the `ShippingMethod` data model and the [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md)'s `ShippingOption` data model. This means you can retrieve the details of a shipping method's shipping option, but you don't manage the links in a pivot table in the database. The shipping option of a shipping method is determined by the `shipping_option_id` property of the `ShippingMethod` data model.

This link allows you to retrieve the shipping option that a shipping method was created from.

This read-only link was added in [Medusa v2.10.0](https://github.com/medusajs/medusa/releases/tag/v2.10.0)

### Retrieve with Query

To retrieve the shipping option of a shipping method with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `shipping_option.*` in `fields`:

### query.graph

```ts
const { data: shippingMethods } = await query.graph({
  entity: "shipping_method",
  fields: [
    "shipping_option.*",
  ],
})

// shippingMethods[0].shipping_option
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: shippingMethods } = useQueryGraphStep({
  entity: "shipping_method",
  fields: [
    "shipping_option.*",
  ],
})

// shippingMethods[0].shipping_option
```

## Order Module

The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management features.

Medusa defines a link between the `Cart` and `Order` data models. The cart is linked to the order created once the cart is completed.

![A diagram showcasing an example of how data models from the Cart and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728375735/Medusa%20Resources/cart-order_ijwmfs.jpg)

### Retrieve with Query

To retrieve the order of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`:

### query.graph

```ts
const { data: carts } = await query.graph({
  entity: "cart",
  fields: [
    "order.*",
  ],
})

// carts[0].order
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: [
    "order.*",
  ],
})

// carts[0].order
```

### Manage with Link

To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.ORDER]: {
    order_id: "order_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.ORDER]: {
    order_id: "order_123",
  },
})
```

***

## Payment Module

The [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md) handles payment processing and management.

Medusa defines a link between the `Cart` and `PaymentCollection` data models. A cart has a payment collection which holds all the authorized payment sessions and payments made related to the cart.

![A diagram showcasing an example of how data models from the Cart and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg)

### Retrieve with Query

To retrieve the payment collection of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_collection.*` in `fields`:

### query.graph

```ts
const { data: carts } = await query.graph({
  entity: "cart",
  fields: [
    "payment_collection.*",
  ],
})

// carts[0].payment_collection
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: [
    "payment_collection.*",
  ],
})

// carts[0].payment_collection
```

### Manage with Link

To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.PAYMENT]: {
    payment_collection_id: "paycol_123",
  },
})
```

### createRemoteLinkStep

```ts
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.PAYMENT]: {
    payment_collection_id: "paycol_123",
  },
})
```

***

## Product Module

Medusa defines read-only links between:

- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `Product` data model. This means you can retrieve the details of a line item's product, but you don't manage the links in a pivot table in the database. The product of a line item is determined by the `product_id` property of the `LineItem` data model.
- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `ProductVariant` data model. This means you can retrieve the details of a line item's variant, but you don't manage the links in a pivot table in the database. The variant of a line item is determined by the `variant_id` property of the `LineItem` data model.

### Retrieve with Query

To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`:

To retrieve the product, pass `product.*` in `fields`.

### query.graph

```ts
const { data: lineItems } = await query.graph({
  entity: "line_item",
  fields: [
    "variant.*",
  ],
})

// lineItems.variant
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: lineItems } = useQueryGraphStep({
  entity: "line_item",
  fields: [
    "variant.*",
  ],
})

// lineItems.variant
```

***

## Promotion Module

The [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md) provides discount features.

Medusa defines a link between the `Cart` and `Promotion` data models. This indicates the promotions applied on a cart.

![A diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg)

Medusa also defines a read-only link between the `LineItemAdjustment` and `Promotion` data models. This means you can retrieve the details of the promotion applied on a line item, but you don't manage the links in a pivot table in the database. The promotion of a line item is determined by the `promotion_id` property of the `LineItemAdjustment` data model.

### Retrieve with Query

To retrieve the promotions of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `promotions.*` in `fields`:

To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`.

### query.graph

```ts
const { data: carts } = await query.graph({
  entity: "cart",
  fields: [
    "promotions.*",
  ],
})

// carts[0].promotions
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: [
    "promotions.*",
  ],
})

// carts[0].promotions
```

### Manage with Link

To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.PROMOTION]: {
    promotion_id: "promo_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.PROMOTION]: {
    promotion_id: "promo_123",
  },
})
```

***

## Region Module

Medusa defines a read-only link between the `Cart` data model and the [Region Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md)'s `Region` data model. This means you can retrieve the details of a cart's region, but you don't manage the links in a pivot table in the database. The region of a cart is determined by the `region_id` property of the `Cart` data model.

### Retrieve with Query

To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`:

### query.graph

```ts
const { data: carts } = await query.graph({
  entity: "cart",
  fields: [
    "region.*",
  ],
})

// carts[0].region
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: [
    "region.*",
  ],
})

// carts[0].region
```

***

## Sales Channel Module

Medusa defines a read-only link between the `Cart` data model and the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md)'s `SalesChannel` data model. This means you can retrieve the details of a cart's sales channel, but you don't manage the links in a pivot table in the database. The sales channel of a cart is determined by the `sales_channel_id` property of the `Cart` data model.

### Retrieve with Query

To retrieve the sales channel of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`:

### query.graph

```ts
const { data: carts } = await query.graph({
  entity: "cart",
  fields: [
    "sales_channel.*",
  ],
})

// carts[0].sales_channel
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: [
    "sales_channel.*",
  ],
})

// carts[0].sales_channel
```


# Cart Module

In this section of the documentation, you will find resources to learn more about the Cart Module and how to use it in your application.

Medusa has cart related features available out-of-the-box through the Cart Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Cart Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Cart Features

- [Cart Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/concepts/index.html.md): Store and manage carts, including their addresses, line items, shipping methods, and more.
- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/promotions/index.html.md): Apply promotions or discounts to line items and shipping methods by adding adjustment lines that are factored into their subtotals.
- [Apply Tax Lines](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/tax-lines/index.html.md): Apply tax lines to line items and shipping methods.
- [Cart Scoping](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/links-to-other-modules/index.html.md): When used in the Medusa application, Medusa creates links to other Commerce Modules, scoping a cart to a sales channel, region, and a customer.

***

## How to Use the Cart Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-cart.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createCartStep = createStep(
  "create-cart",
  async ({}, { container }) => {
    const cartModuleService = container.resolve(Modules.CART)

    const cart = await cartModuleService.createCarts({
      currency_code: "usd",
      shipping_address: {
        address_1: "1512 Barataria Blvd",
        country_code: "us",
      },
      items: [
        {
          title: "Shirt",
          unit_price: 1000,
          quantity: 1,
        },
      ],
    })

    return new StepResponse({ cart }, cart.id)
  },
  async (cartId, { container }) => {
    if (!cartId) {
      return
    }
    const cartModuleService = container.resolve(Modules.CART)

    await cartModuleService.deleteCarts([cartId])
  }
)

export const createCartWorkflow = createWorkflow(
  "create-cart",
  () => {
    const { cart } = createCartStep()

    return new WorkflowResponse({
      cart,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createCartWorkflow } from "../../workflows/create-cart"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createCartWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createCartWorkflow } from "../workflows/create-cart"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createCartWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createCartWorkflow } from "../workflows/create-cart"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createCartWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Promotions Adjustments in Carts

In this document, you’ll learn how a promotion is applied to a cart’s line items and shipping methods using adjustment lines.

## What are Adjustment Lines?

An adjustment line indicates a change to an item or a shipping method’s amount. It’s used to apply promotions or discounts on a cart.

The [LineItemAdjustment](https://docs.medusajs.com/references/cart/models/LineItemAdjustment/index.html.md) data model represents changes on a line item, and the [ShippingMethodAdjustment](https://docs.medusajs.com/references/cart/models/ShippingMethodAdjustment/index.html.md) data model represents changes on a shipping method.

![A diagram showcasing the relations between other data models and adjustment line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534248/Medusa%20Resources/cart-adjustments_k4sttb.jpg)

The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. Also, the ID of the applied promotion is stored in the `promotion_id` property of the adjustment line.

***

## Discountable Option

The [LineItem](https://docs.medusajs.com/references/cart/models/LineItem/index.html.md) data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default.

When disabled, a promotion can’t be applied to a line item. In the context of the Promotion Module, the promotion isn’t applied to the line item even if it matches its rules.

***

## Promotion Actions

When using the Cart and Promotion modules together, such as in the Medusa application, use the [computeActions method of the Promotion Module’s main service](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). It retrieves the actions of line items and shipping methods.

Learn more about actions in the [Promotion Module’s documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md).

For example:

```ts collapsibleLines="1-8" expandButtonLabel="Show Imports"
import {
  ComputeActionAdjustmentLine,
  ComputeActionItemLine,
  ComputeActionShippingLine,
  // ...
} from "@medusajs/framework/types"

// retrieve the cart
const cart = await cartModuleService.retrieveCart("cart_123", {
  relations: [
    "items.adjustments",
    "shipping_methods.adjustments",
  ],
})

// retrieve line item adjustments
const lineItemAdjustments: ComputeActionItemLine[] = []
cart.items.forEach((item) => {
  const filteredAdjustments = item.adjustments?.filter(
    (adjustment) => adjustment.code !== undefined
  ) as unknown as ComputeActionAdjustmentLine[]
  if (filteredAdjustments.length) {
    lineItemAdjustments.push({
      ...item,
      adjustments: filteredAdjustments,
    })
  }
})

// retrieve shipping method adjustments
const shippingMethodAdjustments: ComputeActionShippingLine[] =
  []
cart.shipping_methods.forEach((shippingMethod) => {
  const filteredAdjustments =
    shippingMethod.adjustments?.filter(
      (adjustment) => adjustment.code !== undefined
    ) as unknown as ComputeActionAdjustmentLine[]
  if (filteredAdjustments.length) {
    shippingMethodAdjustments.push({
      ...shippingMethod,
      adjustments: filteredAdjustments,
    })
  }
})

// compute actions
const actions = await promotionModuleService.computeActions(
  ["promo_123"],
  {
    items: lineItemAdjustments,
    shipping_methods: shippingMethodAdjustments,
  }
)
```

The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately.

Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the cart’s line item and the shipping method’s adjustments.

```ts collapsibleLines="1-8" expandButtonLabel="Show Imports"
import {
  AddItemAdjustmentAction,
  AddShippingMethodAdjustment,
  // ...
} from "@medusajs/framework/types"

// ...

await cartModuleService.setLineItemAdjustments(
  cart.id,
  actions.filter(
    (action) => action.action === "addItemAdjustment"
  ) as AddItemAdjustmentAction[]
)

await cartModuleService.setShippingMethodAdjustments(
  cart.id,
  actions.filter(
    (action) =>
      action.action === "addShippingMethodAdjustment"
  ) as AddShippingMethodAdjustment[]
)
```


# Tax Lines in Cart Module

In this document, you’ll learn about tax lines in a cart and how to retrieve tax lines with the Tax Module.

## What are Tax Lines?

A tax line indicates the tax rate of a line item or a shipping method. The [LineItemTaxLine data model](https://docs.medusajs.com/references/cart/models/LineItemTaxLine/index.html.md) represents a line item’s tax line, and the [ShippingMethodTaxLine data model](https://docs.medusajs.com/references/cart/models/ShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line.

![A diagram showcasing the relation between other data models and the tax line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534431/Medusa%20Resources/cart-tax-lines_oheaq6.jpg)

***

## Tax Inclusivity

By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount, and then adding them to the item/method’s subtotal.

However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes.

So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal.

The following diagram is a simplified showcase of how a subtotal is calculated from the taxes perspective.

![A diagram showing an example of calculating the subtotal of a line item using its taxes](https://res.cloudinary.com/dza7lstvk/image/upload/v1711535295/Medusa%20Resources/cart-tax-inclusive_shpr3t.jpg)

For example, if a line item's amount is `5000`, the tax rate is `10`, and tax inclusivity is enabled, the tax amount is 10% of `5000`, which is `500`, making the unit price of the line item `4500`.

***

## Retrieve Tax Lines

When using the Cart and Tax modules together, you can use the `getTaxLines` method of the Tax Module’s main service. It retrieves the tax lines for a cart’s line items and shipping methods.

```ts
// retrieve the cart
const cart = await cartModuleService.retrieveCart("cart_123", {
  relations: [
    "items.tax_lines",
    "shipping_methods.tax_lines",
    "shipping_address",
  ],
})

// retrieve the tax lines
const taxLines = await taxModuleService.getTaxLines(
  [
    ...(cart.items as TaxableItemDTO[]),
    ...(cart.shipping_methods as TaxableShippingDTO[]),
  ],
  {
    address: {
      ...cart.shipping_address,
      country_code:
        cart.shipping_address.country_code || "us",
    },
  }
)
```

Then, use the returned tax lines to set the line items and shipping methods’ tax lines:

```ts
// set line item tax lines
await cartModuleService.setLineItemTaxLines(
  cart.id,
  taxLines.filter((line) => "line_item_id" in line)
)

// set shipping method tax lines
await cartModuleService.setLineItemTaxLines(
  cart.id,
  taxLines.filter((line) => "shipping_line_id" in line)
)
```


# Links between Currency Module and Other Modules

This document showcases the module links defined between the Currency Module and other Commerce Modules.

## Summary

The Currency Module has the following links to other modules:

Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|StoreCurrency|Currency|Read-only - has one|Learn more|

***

## Store Module

The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol.

Instead, Medusa defines a read-only link between the [Store Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/store/index.html.md)'s `StoreCurrency` data model and the Currency Module's `Currency` data model. Because the link is read-only from the `Store`'s side, you can only retrieve the details of a store's supported currencies, and not the other way around.

### Retrieve with Query

To retrieve the details of a store's currencies with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_currencies.currency.*` in `fields`:

### query.graph

```ts
const { data: stores } = await query.graph({
  entity: "store",
  fields: [
    "supported_currencies.currency.*",
  ],
})

// stores[0].supported_currencies[0].currency
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: stores } = useQueryGraphStep({
  entity: "store",
  fields: [
    "supported_currencies.currency.*",
  ],
})

// stores[0].supported_currencies[0].currency
```


# Currency Module

In this section of the documentation, you will find resources to learn more about the Currency Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/store/index.html.md) to learn how to manage your store's currencies using the dashboard.

Medusa has currency related features available out-of-the-box through the Currency Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Currency Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Currency Features

- [Currency Management and Retrieval](https://docs.medusajs.com/references/currency/listAndCountCurrencies/index.html.md): This module adds all common currencies to your application and allows you to retrieve them.
- [Support Currencies in Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/links-to-other-modules/index.html.md): Other Commerce Modules use currency codes in their data models or operations. Use the Currency Module to retrieve a currency code and its details.

***

## How to Use the Currency Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/retrieve-price-with-currency.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
  transform,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const retrieveCurrencyStep = createStep(
  "retrieve-currency",
  async ({}, { container }) => {
    const currencyModuleService = container.resolve(Modules.CURRENCY)

    const currency = await currencyModuleService
      .retrieveCurrency("usd")

    return new StepResponse({ currency })
  }
)

type Input = {
  price: number
}

export const retrievePriceWithCurrency = createWorkflow(
  "create-currency",
  (input: Input) => {
    const { currency } = retrieveCurrencyStep()

    const formattedPrice = transform({
      input,
      currency,
    }, (data) => {
      return `${data.currency.symbol}${data.input.price}`
    })

    return new WorkflowResponse({
      formattedPrice,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { retrievePriceWithCurrency } from "../../workflows/retrieve-price-with-currency"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await retrievePriceWithCurrency(req.scope)
    .run({
      price: 10,
    })

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await retrievePriceWithCurrency(container)
    .run({
      price: 10,
    })

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await retrievePriceWithCurrency(container)
    .run({
      price: 10,
    })

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Customer Accounts

In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers using the dashboard.

## `has_account` Property

The [Customer data model](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) has a `has_account` property, which is a boolean that indicates whether a customer is registered.

When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`.

When this or another guest customer registers an account with the same email, a new `Customer` record is created with `has_account` set to `true`.

***

## Email Uniqueness

The above behavior means that two `Customer` records may exist with the same email address. However, the main difference is the `has_account` property's value.

So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email address.

***

## Customer Deletion and Email Reuse

When a merchant deletes a customer, the `Customer` record is soft-deleted, meaning it is not permanently removed from the database.

When using the Medusa Application with the [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md), possible confusion may arise in the following scenarios:

1. An admin user is using the email address `john@example.com`, and a customer tries to register with the same email address.
2. An admin user has deleted a customer with the email address `jane@example.com`, and another customer tries to register with the same email address.

In these and similar scenarios, the customer trying to register will receive an error message indicating that the email address is already in use:

```json
{
  "type": "unauthorized",
  "message": "Identity with email already exists"
}
```

To resolve this, you can amend the registration flow to:

1. Retrieve the login token of the existing identity with the same email address.
2. Use the login token when registering the new customer. This will not remove the existing identity but will allow the new customer to register with the same email address.

You can learn more about how to implement this flow in the following guides:

- [Conceptual guide on how to implement this flow with Medusa's authentication routes](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#handling-existing-identities/index.html.md).
- [How-to guide on how to implement this in a storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md).


# Links between Customer Module and Other Modules

This document showcases the module links defined between the Customer Module and other Commerce Modules.

## Summary

The Customer Module has the following links to other modules:

Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|Customer|AccountHolder|Stored - many-to-many|Learn more|
|Cart|Customer|Read-only - has one|Learn more|
|Order|Customer|Read-only - has one|Learn more|

***

## Payment Module

Medusa defines a link between the `Customer` and `AccountHolder` data models, allowing payment providers to save payment methods for a customer, if the payment provider supports it.

This link is available starting from Medusa `v2.5.0`.

### Retrieve with Query

To retrieve the account holder associated with a customer with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`:

### query.graph

```ts
const { data: customers } = await query.graph({
  entity: "customer",
  fields: [
    "account_holder_link.account_holder.*",
  ],
})

// customers[0].account_holder_link?.[0]?.account_holder
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: customers } = useQueryGraphStep({
  entity: "customer",
  fields: [
    "account_holder_link.account_holder.*",
  ],
})

// customers[0].account_holder_link?.[0]?.account_holder
```

### Manage with Link

To manage the account holders of a customer, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.CUSTOMER]: {
    customer_id: "cus_123",
  },
  [Modules.PAYMENT]: {
    account_holder_id: "acchld_123",
  },
})
```

### createRemoteLinkStep

```ts
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.CUSTOMER]: {
    customer_id: "cus_123",
  },
  [Modules.PAYMENT]: {
    account_holder_id: "acchld_123",
  },
})
```

***

## Cart Module

Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `Customer` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the customer of a cart, and not the other way around.

### Retrieve with Query

To retrieve the customer of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`:

### query.graph

```ts
const { data: carts } = await query.graph({
  entity: "cart",
  fields: [
    "customer.*",
  ],
})

// carts.customer
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: [
    "customer.*",
  ],
})

// carts.customer
```

***

## Order Module

Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `Customer` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the customer of an order, and not the other way around.

### Retrieve with Query

To retrieve the customer of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`:

### query.graph

```ts
const { data: orders } = await query.graph({
  entity: "order",
  fields: [
    "customer.*",
  ],
})

// orders.customer
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: orders } = useQueryGraphStep({
  entity: "order",
  fields: [
    "customer.*",
  ],
})

// orders.customer
```


# Customer Module

In this section of the documentation, you will find resources to learn more about the Customer Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers and groups using the dashboard.

Medusa has customer related features available out-of-the-box through the Customer Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Customer Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Customer Features

- [Customer Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/customer-accounts/index.html.md): Store and manage guest and registered customers in your store.
- [Customer Organization](https://docs.medusajs.com/references/customer/models/index.html.md): Organize customers into groups. This has a lot of benefits and supports many use cases, such as provide discounts for specific customer groups using the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md).

***

## How to Use the Customer Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-customer.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createCustomerStep = createStep(
  "create-customer",
  async ({}, { container }) => {
    const customerModuleService = container.resolve(Modules.CUSTOMER)

    const customer = await customerModuleService.createCustomers({
      first_name: "Peter",
      last_name: "Hayes",
      email: "peter.hayes@example.com",
    })

    return new StepResponse({ customer }, customer.id)
  },
  async (customerId, { container }) => {
    if (!customerId) {
      return
    }
    const customerModuleService = container.resolve(Modules.CUSTOMER)

    await customerModuleService.deleteCustomers([customerId])
  }
)

export const createCustomerWorkflow = createWorkflow(
  "create-customer",
  () => {
    const { customer } = createCustomerStep()

    return new WorkflowResponse({
      customer,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createCustomerWorkflow } from "../../workflows/create-customer"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createCustomerWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createCustomerWorkflow } from "../workflows/create-customer"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createCustomerWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createCustomerWorkflow } from "../workflows/create-customer"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createCustomerWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Fulfillment Concepts

In this document, you’ll learn about some basic fulfillment concepts.

## Fulfillment Set

A fulfillment set is a general form or way of fulfillment. For example, shipping is a form of fulfillment, and pick-up is another form of fulfillment. Each of these can be created as fulfillment sets.

A fulfillment set is represented by the [FulfillmentSet data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentSet/index.html.md). All other configurations, options, and management features are related to a fulfillment set, in one way or another.

```ts
const fulfillmentSets = await fulfillmentModuleService.createFulfillmentSets(
  [
    {
      name: "Shipping",
      type: "shipping",
    },
    {
      name: "Pick-up",
      type: "pick-up",
    },
  ]
)
```

***

## Service Zone

A service zone is a collection of geographical zones or areas. It’s used to restrict available shipping options to a defined set of locations.

A service zone is represented by the [ServiceZone data model](https://docs.medusajs.com/references/fulfillment/models/ServiceZone/index.html.md). It’s associated with a fulfillment set, as each service zone is specific to a form of fulfillment. For example, if a customer chooses to pick up items, you can restrict the available shipping options based on their location.

![A diagram showcasing the relation between fulfillment sets, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712329770/Medusa%20Resources/service-zone_awmvfs.jpg)

A service zone can have multiple geographical zones, each represented by the [GeoZone data model](https://docs.medusajs.com/references/fulfillment/models/GeoZone/index.html.md). It holds location-related details to narrow down supported areas, such as country, city, or province code.

The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2).

***

## Shipping Profile

A shipping profile defines a type of items that are shipped in a similar manner. For example, a `default` shipping profile is used for all item types, but the `digital` shipping profile is used for digital items that aren’t shipped and delivered conventionally.

A shipping profile is represented by the [ShippingProfile data model](https://docs.medusajs.com/references/fulfillment/models/ShippingProfile/index.html.md). It only defines the profile’s details, but it’s associated with the shipping options available for the item type.


# Fulfillment Module Provider

In this guide, you’ll learn about the Fulfillment Module Provider and how it's used.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations#manage-fulfillment-providers/index.html.md) to learn how to add a fulfillment provider to a location using the dashboard.

## What is a Fulfillment Module Provider?

A Fulfillment Module Provider handles fulfilling items, typically using a third-party integration.

Fulfillment Module Providers registered in the Fulfillment Module's [options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) are stored and represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md).

![Diagram showcasing the communication between Medusa, the Fulfillment Module Provider, and the third-party fulfillment provider.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746794800/Medusa%20Resources/fulfillment-provider-service_ljsqpq.jpg)

***

## Default Fulfillment Provider

Medusa provides a Manual Fulfillment Provider that acts as a placeholder fulfillment provider. It doesn't process fulfillment and delegates that to the merchant.

This provider is installed by default in your application and you can use it to fulfill items manually.

The identifier of the manual fulfillment provider is `fp_manual_manual`.

***

## How to Create a Custom Fulfillment Provider?

A Fulfillment Module Provider is a module whose service implements the `IFulfillmentProvider` imported from `@medusajs/framework/types`.

The module can have multiple fulfillment provider services, where each are registered as separate fulfillment providers.

Refer to the [Create Fulfillment Module Provider](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) guide to learn how to create a Fulfillment Module Provider.

{/* TODO add link to user guide */}

After you create a fulfillment provider, you can choose it as the default Fulfillment Module Provider for a stock location in the Medusa Admin dashboard.

***

## How are Fulfillment Providers Registered?

### Configure Fulfillment Module's Providers

The Fulfillment Module accepts a `providers` option that allows you to configure the providers registered in your application.

Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) guide.

### Registration on Application Start

When the Medusa application starts, it registers the Fulfillment Module Providers defined in the `providers` option of the Fulfillment Module.

For each Fulfillment Module Provider, the Medusa application finds all fulfillment provider services defined in them to register.

### FulfillmentProvider Data Model

A registered fulfillment provider is represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md) in the Medusa application.

This data model is used to reference a service in the Fulfillment Module Provider and determine whether it's installed in the application.

![Diagram showcasing the FulfillmentProvider data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1746794803/Medusa%20Resources/fulfillment-provider-model_wo2ato.jpg)

The `FulfillmentProvider` data model has the following properties:

- `id`: The unique identifier of the fulfillment provider. The ID's format is `fp_{identifier}_{id}`, where:
  - `identifier` is the value of the `identifier` property in the Fulfillment Module Provider's service.
  - `id` is the value of the `id` property of the Fulfillment Module Provider in `medusa-config.ts`.
- `is_enabled`: A boolean indicating whether the fulfillment provider is enabled.

### How to Remove a Fulfillment Provider?

You can remove a registered fulfillment provider from the Medusa application by removing it from the `providers` option in the Fulfillment Module's configuration.

Then, the next time the Medusa application starts, it will set the `is_enabled` property of the `FulfillmentProvider`'s record to `false`. This allows you to re-enable the fulfillment provider later if needed by adding it back to the `providers` option.


# Item Fulfillment Concepts

In this document, you’ll learn about the concepts related to item fulfillment.

## Fulfillment Data Model

A fulfillment is the shipping and delivery of one or more items to the customer. It’s represented by the [Fulfillment data model](https://docs.medusajs.com/references/fulfillment/models/Fulfillment/index.html.md).

A fulfillment can be created to fulfill orders, [returns](../../order/return/), [exchanges](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md), and [claims](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md).

***

## Fulfillment Processing by a Fulfillment Provider

A fulfillment is associated with a [fulfillment provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/fulfillment-provider/index.html.md) that handles all its processing, such as creating a shipment for the fulfillment’s items.

The fulfillment is also associated with a [shipping option](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/index.html.md) of that provider, which determines how the item is shipped.

![A diagram showcasing the relation between a fulfillment, fulfillment provider, and shipping option](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331947/Medusa%20Resources/fulfillment-shipping-option_jk9ndp.jpg)

***

## data Property of Fulfillment Data Model

The `Fulfillment` data model has a `data` property that holds any necessary data for the third-party fulfillment provider to process the fulfillment.

For example, the `data` property can hold the ID of the fulfillment in the third-party provider. The associated fulfillment provider then uses it whenever it retrieves the fulfillment’s details.

***

## Fulfillment Items

A fulfillment is used to fulfill one or more items. Each item is represented by the [FulfillmentItem data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentItem/index.html.md).

The fulfillment item holds details relevant to fulfilling the item, such as barcode, SKU, and quantity to fulfill.

![A diagram showcasing the relation between fulfillment and fulfillment items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712332114/Medusa%20Resources/fulfillment-item_etzxb0.jpg)

***

## Fulfillment Label

Once a shipment is created for the fulfillment, you can store its tracking number, URL, or other related details as a label, represented by the [FulfillmentLabel data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentLabel/index.html.md).

***

## Fulfillment Status

The [Fulfillment data model](https://docs.medusajs.com/references/fulfillment/models/Fulfillment/index.html.md) has three properties to determine the current status of the fulfillment:

- `packed_at`: The date the fulfillment was packed. If set, the fulfillment has been packed.
- `shipped_at`: The date the fulfillment was shipped. If set, the fulfillment has been shipped.
- `delivered_at`: The date the fulfillment was delivered. If set, the fulfillment has been delivered.


# Links between Fulfillment Module and Other Modules

This document showcases the module links defined between the Fulfillment Module and other Commerce Modules.

## Summary

The Fulfillment Module has the following links to other modules:

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|ShippingMethod|ShippingOption|Read-only - has one|Learn more|
|Order|Fulfillment|Stored - one-to-many|Learn more|
|Return|Fulfillment|Stored - one-to-many|Learn more|
|PriceSet|ShippingOption|Stored - many-to-one|Learn more|
|Product|ShippingProfile|Stored - many-to-one|Learn more|
|StockLocation|FulfillmentProvider|Stored - one-to-many|Learn more|
|StockLocation|FulfillmentSet|Stored - one-to-many|Learn more|

***

## Cart Module

Medusa defines a read-only link between the `ShippingMethod` data model of the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md) and the `ShippingOption` data model. This means you can retrieve the details of a shipping method's shipping option, but you don't manage the links in a pivot table in the database. The shipping option of a shipping method is determined by the `shipping_option_id` property of the `ShippingMethod` data model.

This link allows you to retrieve the shipping option that a shipping method was created from.

This read-only link was added in [Medusa v2.10.0](https://github.com/medusajs/medusa/releases/tag/v2.10.0)

### Retrieve with Query

To retrieve the shipping option of a shipping method with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `shipping_option.*` in `fields`:

### query.graph

```ts
const { data: shippingMethods } = await query.graph({
  entity: "shipping_method",
  fields: [
    "shipping_option.*",
  ],
})

// shippingMethods[0].shipping_option
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: shippingMethods } = useQueryGraphStep({
  entity: "shipping_method",
  fields: [
    "shipping_option.*",
  ],
})

// shippingMethods[0].shipping_option
```

***

## Order Module

The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management functionalities.

Medusa defines a link between the `Fulfillment` and `Order` data models. A fulfillment is created for an orders' items.

![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716549903/Medusa%20Resources/order-fulfillment_h0vlps.jpg)

A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models.

![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399052/Medusa%20Resources/Social_Media_Graphics_2024_Order_Return_vetimk.jpg)

### Retrieve with Query

To retrieve the order of a fulfillment with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`:

To retrieve the return, pass `return.*` in `fields`.

### query.graph

```ts
const { data: fulfillments } = await query.graph({
  entity: "fulfillment",
  fields: [
    "order.*",
  ],
})

// fulfillments.order
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: fulfillments } = useQueryGraphStep({
  entity: "fulfillment",
  fields: [
    "order.*",
  ],
})

// fulfillments.order
```

### Manage with Link

To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.FULFILLMENT]: {
    fulfillment_id: "ful_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.FULFILLMENT]: {
    fulfillment_id: "ful_123",
  },
})
```

***

## Pricing Module

The Pricing Module provides features to store, manage, and retrieve the best prices in a specified context.

Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set.

![A diagram showcasing an example of how data models from the Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg)

### Retrieve with Query

To retrieve the price set of a shipping option with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`:

### query.graph

```ts
const { data: shippingOptions } = await query.graph({
  entity: "shipping_option",
  fields: [
    "price_set_link.*",
  ],
})

// shippingOptions[0].price_set_link?.price_set_id
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: shippingOptions } = useQueryGraphStep({
  entity: "shipping_option",
  fields: [
    "price_set_link.*",
  ],
})

// shippingOptions[0].price_set_link?.price_set_id
```

### Manage with Link

To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.FULFILLMENT]: {
    shipping_option_id: "so_123",
  },
  [Modules.PRICING]: {
    price_set_id: "pset_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.FULFILLMENT]: {
    shipping_option_id: "so_123",
  },
  [Modules.PRICING]: {
    price_set_id: "pset_123",
  },
})
```

***

## Product Module

Medusa defines a link between the `ShippingProfile` data model and the `Product` data model of the Product Module. Each product must belong to a shipping profile.

This link is introduced in [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0).

### Retrieve with Query

To retrieve the products of a shipping profile with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`:

### query.graph

```ts
const { data: shippingProfiles } = await query.graph({
  entity: "shipping_profile",
  fields: [
    "products.*",
  ],
})

// shippingProfiles[0].products
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: shippingProfiles } = useQueryGraphStep({
  entity: "shipping_profile",
  fields: [
    "products.*",
  ],
})

// shippingProfiles[0].products
```

### Manage with Link

To manage the shipping profile of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  [Modules.FULFILLMENT]: {
    shipping_profile_id: "sp_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  [Modules.FULFILLMENT]: {
    shipping_profile_id: "sp_123",
  },
})
```

***

## Stock Location Module

The Stock Location Module provides features to manage stock locations in a store.

Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. A fulfillment set can be conditioned to a specific stock location.

![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1712567101/Medusa%20Resources/fulfillment-stock-location_nlkf7e.jpg)

Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location.

![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399492/Medusa%20Resources/fulfillment-provider-stock-location_b0mulo.jpg)

### Retrieve with Query

To retrieve the stock location of a fulfillment set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `location.*` in `fields`:

To retrieve the stock location of a fulfillment provider, pass `locations.*` in `fields`.

### query.graph

```ts
const { data: fulfillmentSets } = await query.graph({
  entity: "fulfillment_set",
  fields: [
    "location.*",
  ],
})

// fulfillmentSets[0].location
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: fulfillmentSets } = useQueryGraphStep({
  entity: "fulfillment_set",
  fields: [
    "location.*",
  ],
})

// fulfillmentSets[0].location
```

### Manage with Link

To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.STOCK_LOCATION]: {
    stock_location_id: "sloc_123",
  },
  [Modules.FULFILLMENT]: {
    fulfillment_set_id: "fset_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.STOCK_LOCATION]: {
    stock_location_id: "sloc_123",
  },
  [Modules.FULFILLMENT]: {
    fulfillment_set_id: "fset_123",
  },
})
```


# Fulfillment Module Options

In this document, you'll learn about the options of the Fulfillment Module. You can pass these options in `medusa-config.ts`.

## providers

The `providers` option is an array of [Fulfillment Module Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/fulfillment-provider/index.html.md).

When the Medusa application starts, these providers are registered and can be used to process fulfillments.

For example:

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/fulfillment",
      options: {
        providers: [
          {
            resolve: `@medusajs/medusa/fulfillment-manual`,
            id: "manual",
            options: {
              // provider options...
            },
          },
        ],
      },
    },
  ],
})
```

The `providers` option is an array of objects that accept the following properties:

- `resolve`: A string indicating either the package name  of the module provider or its relative path.
- `id`: A string indicating the provider's unique name or ID.
- `options`: An optional object of the module provider's options.


# Fulfillment Module

In this section of the documentation, you will find resources to learn more about the Fulfillment Module and how to use it in your application.

Refer to the Medusa Admin User Guide to learn how to use the dashboard to:

- [Manage order fulfillments](https://docs.medusajs.com/user-guide/orders/fulfillments/index.html.md).
- [Manage shipping options and profiles](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/index.html.md).

Medusa has fulfillment related features available out-of-the-box through the Fulfillment Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Fulfillment Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Fulfillment Features

- [Fulfillment Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/item-fulfillment/index.html.md): Create fulfillments and keep track of their status, items, and more.
- [Integrate Third-Party Fulfillment Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/fulfillment-provider/index.html.md): Create third-party fulfillment providers to provide customers with shipping options and fulfill their orders.
- [Restrict By Location and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/index.html.md): Shipping options can be restricted to specific geographical locations. You can also specify custom rules to restrict shipping options.
- [Support Different Fulfillment Forms](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/concepts/index.html.md): Support various fulfillment forms, such as shipping or pick up.
- [Tiered Pricing and Price Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules/index.html.md): Set prices for shipping options with tiers and rules, allowing you to create complex pricing strategies.

***

## How to Use the Fulfillment Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-fulfillment.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createFulfillmentStep = createStep(
  "create-fulfillment",
  async ({}, { container }) => {
    const fulfillmentModuleService = container.resolve(Modules.FULFILLMENT)

    const fulfillment = await fulfillmentModuleService.createFulfillment({
      location_id: "loc_123",
      provider_id: "webshipper",
      delivery_address: {
        country_code: "us",
        city: "Strongsville",
        address_1: "18290 Royalton Rd",
      },
      items: [
        {
          title: "Shirt",
          sku: "SHIRT",
          quantity: 1,
          barcode: "123456",
        },
      ],
      labels: [],
      order: {},
    })

    return new StepResponse({ fulfillment }, fulfillment.id)
  },
  async (fulfillmentId, { container }) => {
    if (!fulfillmentId) {
      return
    }
    const fulfillmentModuleService = container.resolve(Modules.FULFILLMENT)

    await fulfillmentModuleService.deleteFulfillment(fulfillmentId)
  }
)

export const createFulfillmentWorkflow = createWorkflow(
  "create-fulfillment",
  () => {
    const { fulfillment } = createFulfillmentStep()

    return new WorkflowResponse({
      fulfillment,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createFulfillmentWorkflow } from "../../workflows/create-fuilfillment"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createFulfillmentWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createFulfillmentWorkflow } from "../workflows/create-fuilfillment"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createFulfillmentWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createFulfillmentWorkflow } from "../workflows/create-fuilfillment"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createFulfillmentWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***

## Configure Fulfillment Module

The Fulfillment Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) for details on the module's options.

***


# Shipping Option

In this document, you’ll learn about shipping options and their rules.

## What is a Shipping Option?

A shipping option is a way of shipping an item. Each fulfillment provider offers a set of shipping options. For example, a provider may offer a shipping option for express shipping and another for standard shipping.

When the customer places an order, they choose a shipping option to fulfill their items.

A shipping option is represented by the [ShippingOption data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOption/index.html.md).

***

## Service Zone Restrictions

A shipping option is restricted by a service zone, which limits the locations where the shipping option can be used.

For example, a fulfillment provider may have a shipping option that can be used in the United States and another in Canada.

![A diagram showcasing the relation between shipping options and service zones.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712330831/Medusa%20Resources/shipping-option-service-zone_pobh6k.jpg)

Service zones can be more restrictive, such as limiting to certain cities or province codes.

The province code is always in lowercase and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2).

![A diagram showcasing the relation between shipping options, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331186/Medusa%20Resources/shipping-option-service-zone-city_m5sxod.jpg)

***

## Shipping Option Rules

You can restrict shipping options by custom rules, such as the item’s weight or the customer group.

You can also restrict a shipping option's price based on specific conditions. For example, you can make a shipping option's price free based on the cart total. Learn more in the Pricing Module's [Price Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules#how-to-set-rules-on-a-price/index.html.md) guide.

These rules are represented by the [ShippingOptionRule data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionRule/index.html.md). Its properties define the custom rules:

- `attribute`: The name of a property or table that the rule applies to. For example, `customer_group`.
- `operator`: The operator used in the condition. For example:
  - To allow multiple values, use the operator `in`, which validates that the provided values are in the rule’s values.
  - To create a negation condition that considers `value` against the rule, use `nin`, which validates that the provided values aren’t in the rule’s values.
- `value`: One or more values.

![A diagram showcasing the relation between shipping option and shipping option rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331340/Medusa%20Resources/shipping-option-rule_oosopf.jpg)

A shipping option can have multiple rules. For example, you can add rules to a shipping option so that it's available if the customer belongs to the VIP group and the total weight is less than 2000g.

![A diagram showcasing how a shipping option can have multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331462/Medusa%20Resources/shipping-option-rule-2_ylaqdb.jpg)

***

## Shipping Profiles and Types

A shipping option belongs to a type and a profile.

A shipping option type defines a group of shipping options with shared shipping characteristics. For example, a shipping option’s type may be `express`, while another may be `standard`. The type is represented by the [ShippingOptionType data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionType/index.html.md).

A shipping profile defines a group of items (such as products) that are shipped in a similar manner. For example, the "Standard" shipping profile applies to all products, whereas the "Digital" shipping profile applies to digital products. Shipping profiles are represented by the [ShippingProfile data model](https://docs.medusajs.com/references/fulfillment/models/ShippingProfile/index.html.md).

***

## data Property

When fulfilling an item, you might use a third-party fulfillment provider that requires additional custom data to be passed along from the checkout or order creation process.

The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant for creating and processing a fulfillment later.


# Inventory Concepts

In this guide, you'll learn about the main concepts in the Inventory Module and how data is stored and connected.

## InventoryItem

An inventory item, represented by the [InventoryItem data model](https://docs.medusajs.com/references/inventory-next/models/InventoryItem/index.html.md), is a stock-kept item whose inventory can be managed. For example, a product.

The `InventoryItem` data model holds details about the stock item. It connects to other data models like `InventoryLevel` that store stock and quantity details.

![A diagram showcasing the relation between data models in the Inventory Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658103/Medusa%20Resources/inventory-architecture_kxr2ql.png)

### Inventory Shipping Requirement

An inventory item has a `requires_shipping` field that indicates whether the item needs shipping. This field is enabled by default.

For example, if you're selling a digital license with limited stock that doesn't need shipping, you can set this field to `false`.

When a customer buys a product variant in the Medusa application, this field determines whether the item needs shipping. Learn more in the [Configure Selling Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/selling-products/index.html.md) guide.

***

## InventoryLevel

An inventory level, represented by the [InventoryLevel data model](https://docs.medusajs.com/references/inventory-next/models/InventoryLevel/index.html.md), stores the inventory and quantity details of an inventory item in a specific location.

It has three quantity properties:

- `stocked_quantity`: The available stock quantity of an item in the associated location.
- `reserved_quantity`: The quantity reserved from the available `stocked_quantity`. This quantity is still in stock but unavailable when checking if an item is available.
  - For example, when an order is placed but not yet fulfilled, the ordered quantity is reserved from available stock.
- `incoming_quantity`: The incoming stock quantity of an item into the associated location. This property doesn't affect the `stocked_quantity` or availability checks.

### Associated Location

The inventory level's location is set in the `location_id` property. Medusa links the `InventoryLevel` data model with the `StockLocation` data model from the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md).

***

## ReservationItem

A reservation item, represented by the [ReservationItem](https://docs.medusajs.com/references/inventory-next/models/ReservationItem/index.html.md) data model, represents unavailable quantity of an inventory item in a location.

When an order is placed, Medusa creates a reservation item for each inventory item in the order. The reservation item stores the reserved quantity of the inventory item in the location associated with the order's sales channel.

You can also use reservation items for custom use cases. For example, if you're selling event tickets, you can create a reservation item when a customer selects a ticket. Then, you can remove the reservation item if the customer doesn't complete the purchase within a specific time.

The reserved quantity is linked to a location, so it has a similar relationship to the `InventoryLevel` with the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md).


# Inventory Module in Medusa Flows

In this guide, you'll learn how Medusa uses the Inventory Module in its commerce flows, including product variant creation, adding to cart, order placement, order fulfillment, and order return.

## Product Variant Creation

When a product variant is created and its `manage_inventory` property's value is `true` and the variant's `inventory_items` are set, the Medusa application creates an inventory item associated with that product variant.

This flow is implemented within the [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md)

![A diagram showcasing how the Inventory Module is used in the product variant creation flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709661511/Medusa%20Resources/inventory-product-create_khz2hk.jpg)

***

## Add to Cart

When a product variant with `manage_inventory` set to `true` is added to the cart, the Medusa application checks whether there's sufficient stocked quantity. If not, an error is thrown and the product variant won't be added to the cart.

This flow is implemented within the [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md)

![A diagram showcasing how the Inventory Module is used in the add to cart flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709711645/Medusa%20Resources/inventory-cart-flow_achwq9.jpg)

***

## Order is Placed

When an order is placed, the Medusa application creates a reservation item for each product variant with `manage_inventory` set to `true`.

This flow is implemented within the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md)

![A diagram showcasing how the Inventory Module is used in the order placement flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712005/Medusa%20Resources/inventory-order-placed_qdxqdn.jpg)

***

## Order Fulfillment

When an item in an order is fulfilled and the associated variant has its `manage_inventory` property set to `true`, the Medusa application:

- Subtracts the `reserved_quantity` from the `stocked_quantity` in the inventory level associated with the variant's inventory item.
- Resets the `reserved_quantity` to `0`.
- Deletes the associated reservation item.

This flow is implemented within the [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md)

![A diagram showcasing how the Inventory Module is used in the order fulfillment flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712390/Medusa%20Resources/inventory-order-fulfillment_o9wdxh.jpg)

***

## Order Return

When an item in an order is returned and the associated variant has its `manage_inventory` property set to `true`, the Medusa application increments the `stocked_quantity` of the inventory item's level with the returned quantity.

This flow is implemented within the [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md)

![A diagram showcasing how the Inventory Module is used in the order return flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712457/Medusa%20Resources/inventory-order-return_ihftyk.jpg)

### Dismissed Returned Items

If a returned item is considered damaged or is dismissed, its quantity doesn't increment the `stocked_quantity` of the inventory item's level.


# Inventory Kits

In this guide, you'll learn how inventory kits can be used in the Medusa application to support use cases like multi-part products, bundled products, and shared inventory across products.

Refer to the following user guides to learn how to use the Medusa Admin dashboard to:

- [Create Multi-Part Products](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md).
- [Create Bundled Products](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md).

## What is an Inventory Kit?

An inventory kit is a collection of inventory items that are linked to a single product variant. These inventory items can be used to represent different parts of a product, or to represent a bundle of products.

The Medusa application links inventory items from the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) to product variants in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md). Each variant can have multiple inventory items, and these inventory items can be re-used or shared across variants.

Using inventory kits, you can implement use cases like:

- [Multi-part products](#multi-part-products): A product that consists of multiple parts, each with its own inventory item.
- [Bundled products](#bundled-products): A product that is sold as a bundle, where each variant in the bundle product can re-use the inventory items of another product that should be sold as part of the bundle.

***

## Multi-Part Products

Consider your store sells bicycles that consist of a frame, wheels, and seats, and you want to manage the inventory of these parts separately.

To implement this in Medusa, you can:

- Create inventory items for each of the different parts.
- For each bicycle product, add a variant whose inventory kit consists of the inventory items of each of the parts.

Then, whenever a customer purchases a bicycle, the inventory of each part is updated accordingly. You can also use the `required_quantity` of the variant's inventory items to set how much quantity is consumed of the part's inventory when a bicycle is sold. For example, the bicycle's wheels require 2 wheels inventory items to be sold when a bicycle is sold.

![Diagram showcasing how a variant is linked to multi-part inventory items](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414257/Medusa%20Resources/multi-part-product_kepbnx.jpg)

### Create Multi-Part Product

Using the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md), you can create a multi-part product by creating its inventory items first, then assigning these inventory items to the product's variant(s).

Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the inventory items:

```ts highlights={multiPartsHighlights1}
import { 
  createInventoryItemsWorkflow, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { createWorkflow } from "@medusajs/framework/workflows-sdk"

export const createMultiPartProductsWorkflow = createWorkflow(
  "create-multi-part-products",
  () => {
    // Alternatively, you can create a stock location
    const { data: stockLocations } = useQueryGraphStep({
      entity: "stock_location",
      fields: ["*"],
      filters: {
        name: "European Warehouse",
      },
    })

    const inventoryItems = createInventoryItemsWorkflow.runAsStep({
      input: {
        items: [
          {
            sku: "FRAME",
            title: "Frame",
            location_levels: [
              {
                stocked_quantity: 100,
                location_id: stockLocations[0].id,
              },
            ],
          },
          {
            sku: "WHEEL",
            title: "Wheel",
            location_levels: [
              {
                stocked_quantity: 100,
                location_id: stockLocations[0].id,
              },
            ],
          },
          {
            sku: "SEAT",
            title: "Seat",
            location_levels: [
              {
                stocked_quantity: 100,
                location_id: stockLocations[0].id,
              },
            ],
          },
        ],
      },
    })

    // TODO create the product
  }
)
```

You start by retrieving the stock location to create the inventory items in. Alternatively, you can [create a stock location](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md).

Then, you create the inventory items that the product variant consists of.

Next, create the product and pass the inventory item's IDs to the product's variant:

```ts highlights={multiPartHighlights2}
import { 
  // ...
  transform,
} from "@medusajs/framework/workflows-sdk"
import { 
  // ...
  createProductsWorkflow,
} from "@medusajs/medusa/core-flows"

export const createMultiPartProductsWorkflow = createWorkflow(
  "create-multi-part-products",
  () => {
    // ...

    const inventoryItemIds = transform({
      inventoryItems,
    }, (data) => {
      return data.inventoryItems.map((inventoryItem) => {
        return {
          inventory_item_id: inventoryItem.id,
          // can also specify required_quantity
        }
      })
    })

    const products = createProductsWorkflow.runAsStep({
      input: {
        products: [
          {
            title: "Bicycle",
            variants: [
              {
                title: "Bicycle - Small",
                prices: [
                  {
                    amount: 100,
                    currency_code: "usd",
                  },
                ],
                options: {
                  "Default Option": "Default Variant",
                },
                inventory_items: inventoryItemIds,
              },
            ],
            options: [
              {
                title: "Default Option",
                values: ["Default Variant"],
              },
            ],
            shipping_profile_id: "sp_123",
          },
        ],
      },
    })
  }
)
```

You prepare the inventory item IDs to pass to the variant using [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK, then pass these IDs to the created product's variant.

You can now [execute the workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md) in [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [scheduled jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md), or [subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).

***

## Bundled Products

While inventory kits support bundled products, some features like custom pricing for a bundle or separate fulfillment for a bundle's items are not supported. To support those features, follow the [Bundled Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/bundled-products/examples/standard/index.html.md) tutorial to learn how to customize the Medusa application to add bundled products.

Consider you have three products: shirt, pants, and shoes. You sell those products separately, but you also want to offer them as a bundle.

![Diagram showcasing products each having their own variants and inventory](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414787/Medusa%20Resources/bundled-product-1_vmzewk.jpg)

You can do that by creating a product, where each variant re-uses the inventory items of each of the shirt, pants, and shoes products.

Then, when the bundled product's variant is purchased, the inventory quantity of the associated inventory items are updated.

![Diagram showcasing a bundled product using the same inventory as the products part of the bundle](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414780/Medusa%20Resources/bundled-product_x94ca1.jpg)

### Create Bundled Product

You can create a bundled product in the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md) by creating the products part of the bundle first, each having its own inventory items. Then, you create the bundled product whose variant(s) have inventory kits composed of inventory items from each of the products part of the bundle.

Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the products part of the bundle:

```ts highlights={bundledHighlights1}
import { 
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import { 
  createProductsWorkflow,
} from "@medusajs/medusa/core-flows"

export const createBundledProducts = createWorkflow(
  "create-bundled-products",
  () => {
    const products = createProductsWorkflow.runAsStep({
      input: {
        products: [
          {
            title: "Shirt",
            shipping_profile_id: "sp_123",
            variants: [
              {
                title: "Shirt",
                prices: [
                  {
                    amount: 10,
                    currency_code: "usd",
                  },
                ],
                options: {
                  "Default Option": "Default Variant",
                },
                manage_inventory: true,
              },
            ],
            options: [
              {
                title: "Default Option",
                values: ["Default Variant"],
              },
            ],
          },
          {
            title: "Pants",
            shipping_profile_id: "sp_123",
            variants: [
              {
                title: "Pants",
                prices: [
                  {
                    amount: 10,
                    currency_code: "usd",
                  },
                ],
                options: {
                  "Default Option": "Default Variant",
                },
                manage_inventory: true,
              },
            ],
            options: [
              {
                title: "Default Option",
                values: ["Default Variant"],
              },
            ],
          },
          {
            title: "Shoes",
            shipping_profile_id: "sp_123",
            variants: [
              {
                title: "Shoes",
                prices: [
                  {
                    amount: 10,
                    currency_code: "usd",
                  },
                ],
                options: {
                  "Default Option": "Default Variant",
                },
                manage_inventory: true,
              },
            ],
            options: [
              {
                title: "Default Option",
                values: ["Default Variant"],
              },
            ],
          },
        ],
      },
    })

    // TODO re-retrieve with inventory
  }
)
```

You create three products and enable `manage_inventory` for their variants, which will create a default inventory item. You can also create the inventory item first for more control over the quantity as explained in [the previous section](#create-multi-part-product).

Next, retrieve the products again but with variant information:

```ts highlights={bundledHighlights2}
import { 
  // ...
  transform,
} from "@medusajs/framework/workflows-sdk"
import { 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"

export const createBundledProducts = createWorkflow(
  "create-bundled-products",
  () => {
    // ...
    const productIds = transform({
      products,
    }, (data) => data.products.map((product) => product.id))

    // @ts-ignore
    const { data: productsWithInventory } = useQueryGraphStep({
      entity: "product",
      fields: [
        "variants.*",
        "variants.inventory_items.*",
      ],
      filters: {
        id: productIds,
      },
    })

    const inventoryItemIds = transform({
      productsWithInventory,
    }, (data) => {
      return data.productsWithInventory.map((product) => {
        return {
          inventory_item_id: product.variants[0].inventory_items?.[0]?.inventory_item_id,
        }
      })
    })

    // create bundled product
  }
)
```

Using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), you retrieve the product again with the inventory items of each variant. Then, you prepare the inventory items to pass to the bundled product's variant.

Finally, create the bundled product:

```ts highlights={bundledProductHighlights3}
export const createBundledProducts = createWorkflow(
  "create-bundled-products",
  () => {
    // ...
    const bundledProduct = createProductsWorkflow.runAsStep({
      input: {
        products: [
          {
            title: "Bundled Clothes",
            shipping_profile_id: "sp_123",
            variants: [
              {
                title: "Bundle",
                prices: [
                  {
                    amount: 30,
                    currency_code: "usd",
                  },
                ],
                options: {
                  "Default Option": "Default Variant",
                },
                inventory_items: inventoryItemIds,
              },
            ],
            options: [
              {
                title: "Default Option",
                values: ["Default Variant"],
              },
            ],
          },
        ],
      },
    }).config({ name: "create-bundled-product" })
  }
)
```

The bundled product has the same inventory items as those of the products part of the bundle.

You can now [execute the workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md) in [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [scheduled jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md), or [subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).


# Links between Inventory Module and Other Modules

This document showcases the module links defined between the Inventory Module and other Commerce Modules.

## Summary

The Inventory Module has the following links to other modules:

Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|ProductVariant|InventoryItem|Stored - many-to-many|Learn more|
|InventoryLevel|StockLocation|Read-only - has many|Learn more|

***

## Product Module

Each product variant has different inventory details. Medusa defines a link between the `ProductVariant` and `InventoryItem` data models.

![A diagram showcasing an example of how data models from the Inventory and Product Module are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658720/Medusa%20Resources/inventory-product_ejnray.jpg)

A product variant whose `manage_inventory` property is enabled has an associated inventory item. Through that inventory's items relations in the Inventory Module, you can manage and check the variant's inventory quantity.

Learn more about product variant's inventory management in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md).

### Retrieve with Query

To retrieve the product variants of an inventory item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variants.*` in `fields`:

### query.graph

```ts
const { data: inventoryItems } = await query.graph({
  entity: "inventory_item",
  fields: [
    "variants.*",
  ],
})

// inventoryItems[0].variants
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: inventoryItems } = useQueryGraphStep({
  entity: "inventory_item",
  fields: [
    "variants.*",
  ],
})

// inventoryItems[0].variants
```

### Manage with Link

To manage the variants of an inventory item, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.PRODUCT]: {
    variant_id: "variant_123",
  },
  [Modules.INVENTORY]: {
    inventory_item_id: "iitem_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.PRODUCT]: {
    variant_id: "variant_123",
  },
  [Modules.INVENTORY]: {
    inventory_item_id: "iitem_123",
  },
})
```

***

## Stock Location Module

Medusa defines a read-only link between the `InventoryLevel` data model and the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md)'s `StockLocation` data model. This means you can retrieve the details of an inventory level's stock locations, but you don't manage the links in a pivot table in the database. The stock location of an inventory level is determined by the `location_id` property of the `InventoryLevel` data model.

### Retrieve with Query

To retrieve the stock locations of an inventory level with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`:

### query.graph

```ts
const { data: inventoryLevels } = await query.graph({
  entity: "inventory_level",
  fields: [
    "stock_locations.*",
  ],
})

// inventoryLevels[0].stock_locations
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: inventoryLevels } = useQueryGraphStep({
  entity: "inventory_level",
  fields: [
    "stock_locations.*",
  ],
})

// inventoryLevels[0].stock_locations
```


# Inventory Module

In this section of the documentation, you will find resources to learn more about the Inventory Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/inventory/index.html.md) to learn how to manage inventory and related features using the dashboard.

Medusa has inventory related features available out-of-the-box through the Inventory Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Inventory Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Inventory Features

- [Inventory Items Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md): Store and manage inventory of any stock-kept item, such as product variants.
- [Inventory Across Locations](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventorylevel/index.html.md): Manage inventory levels across different locations, such as warehouses.
- [Reservation Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#reservationitem/index.html.md): Reserve quantities of inventory items at specific locations for orders or other purposes.
- [Check Inventory Availability](https://docs.medusajs.com/references/inventory-next/confirmInventory/index.html.md): Check whether an inventory item has the necessary quantity for purchase.
- [Inventory Kits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products.

***

## How to Use the Inventory Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-inventory-item.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createInventoryItemStep = createStep(
  "create-inventory-item",
  async ({}, { container }) => {
    const inventoryModuleService = container.resolve(Modules.INVENTORY)

    const inventoryItem = await inventoryModuleService.createInventoryItems({
      sku: "SHIRT",
      title: "Green Medusa Shirt",
      requires_shipping: true,
    })

    return new StepResponse({ inventoryItem }, inventoryItem.id)
  },
  async (inventoryItemId, { container }) => {
    if (!inventoryItemId) {
      return
    }
    const inventoryModuleService = container.resolve(Modules.INVENTORY)

    await inventoryModuleService.deleteInventoryItems([inventoryItemId])
  }
)

export const createInventoryItemWorkflow = createWorkflow(
  "create-inventory-item-workflow",
  () => {
    const { inventoryItem } = createInventoryItemStep()

    return new WorkflowResponse({
      inventoryItem,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createInventoryItemWorkflow } from "../../workflows/create-inventory-item"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createInventoryItemWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createInventoryItemWorkflow } from "../workflows/create-inventory-item"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createInventoryItemWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createInventoryItemWorkflow } from "../workflows/create-inventory-item"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createInventoryItemWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Order Claim

In this guide, you'll learn about order claims.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/claims/index.html.md) to learn how to manage an order's claims using the dashboard.

## What is a Claim?

When a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace that item.

A claim is represented by the [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md).

***

## Claim Type

The `Claim` data model has a `type` property that indicates the type of claim:

- `refund`: The items are returned and the customer receives a refund.
- `replace`: The items are returned and the customer receives new items.

***

## Old and Replacement Items

When you create a claim, a return is also created to handle receiving the old items from the customer. The return is represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md).

Refer to the [Returns](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md) guide to learn more about returns.

If the claim's type is `replace`, the replacement items are represented by the [ClaimItem data model](https://docs.medusajs.com/references/order/models/OrderClaimItem/index.html.md).

***

## Claim Shipping Methods

A claim uses shipping methods to send replacement items to the customer. These methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md).

The shipping methods for returned items are linked to the claim's return, as explained in the [Returns](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md) guide.

***

## Claim Refund

If the claim's type is `refund`, the refund amount is stored in the `refund_amount` property.

The [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the claim.

***

## How Claims Impact an Order's Version

When you confirm a claim, the order's version increases.

Learn more about order versions in the [Order Versioning](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-versioning/index.html.md) guide.


# Order Concepts

In this document, you’ll learn about orders and related concepts

## Order

An order is a purchase made by a customer, either through the storefront or through the API. It is represented by the [Order data model](https://docs.medusajs.com/references/order/models/Order/index.html.md).

An order holds information about:

- The purchased items.
- The customer who made the order.
  - This is stored and handled by the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md).
- Payment and shipping information

## Order Locale

Order locale is available starting [Medusa v2.12.3](https://github.com/medusajs/medusa/releases/tag/v2.12.3).

The `Order` data model has a `locale` property that indicates the locale of the order. This locale is in the [IETF BCP 47 standard](https://gist.github.com/typpo/b2b828a35e683b9bf8db91b5404f1bd1) format, such as `en-US` for American English or `fr-FR` for French (France).

When an order is created from a cart or a draft order, the order inherits their locale. This ensures that all item details in the order, such as product titles and descriptions, are presented in the correct language if translations are available.

When order items are changed through an edit, claim, or exchange, the new items added to the order will also have their details in the order's locale if translations are available.

You can also edit an order's locale using the [Update Order](https://docs.medusajs.com/api/admin#orders_postordersid) API route.

***

## Order Items

The items purchased in an order are represented by the [OrderItem data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). An order can have multiple items.

![A diagram showcasing the relation between an order and its items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712304722/Medusa%20Resources/order-order-items_uvckxd.jpg)

### Item’s Product Details

The details of the purchased products are represented by the [LineItem data model](https://docs.medusajs.com/references/order/models/OrderLineItem/index.html.md). Not only does a line item hold the details of the product, but also details related to its price, adjustments due to promotions, and taxes.

***

## Order’s Shipping Method

An order has one or more shipping methods used to handle item shipment.

Each shipping method is represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md) that holds its details. The shipping method is linked to the order through the [OrderShipping data model](https://docs.medusajs.com/references/order/models/OrderShipping/index.html.md).

![A diagram showcasing the relation between an order and its items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1719570409/Medusa%20Resources/order-shipping-method_tkggvd.jpg)

### data Property

When fulfilling an order, you can use a third-party fulfillment provider that requires additional custom data to be passed along from the order creation process.

The `OrderShippingMethod` data model has a `data` property. It’s an object used to store custom data relevant later for fulfillment.

The Medusa application passes the `data` property to the Fulfillment Module when fulfilling items.

***

## Order Totals

An order’s total amounts (including tax total, total after an item is returned, etc…) are represented by the [OrderSummary data model](https://docs.medusajs.com/references/order/models/OrderSummary/index.html.md).

Refer to the [Retrieve Order Totals](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-totals/index.html.md) guide to learn how to retrieve an order’s totals.

***

## Order Payments

Payments made on an order, whether they’re capture or refund payments, are recorded as transactions represented by the [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md).

An order can have multiple transactions. The sum of these transactions must be equal to the order summary’s total. Otherwise, there’s an outstanding amount.

Refer to the [Transactions](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions/index.html.md) guide to learn more.


# Custom Order Display ID

In this guide, you'll learn how to customize the display ID of orders in Medusa.

This feature is available since [Medusa v2.12.0](https://github.com/medusajs/medusa/releases/tag/v2.12.0).

## Default Display ID

By default, Medusa stores the display ID of orders in the `display_id` property of the [Order data model](https://docs.medusajs.com/references/order/models/Order/index.html.md). The display ID is a serial integer that starts at 1 and increments with each new order.

For example:

```json
{
  "id": "order_123",
  "display_id": 1,
  // other properties...
}
```

***

## Custom Display ID

In some cases, you might want to use a custom display ID for orders. This is useful for integrating with external systems or providing a more user-friendly order identifier.

The `Order` data model has a `custom_display_id` property that stores a custom display ID you generate.

You can define the logic for generating this ID in the `generateCustomDisplayId` module option set in `medusa-config.ts`.

For example:

```ts title="medusa-config.ts"
// other imports...
import { Modules } from "@medusajs/framework/utils"
import { OrderTypes, Context } from "@medusajs/framework/types"

module.exports = defineConfig({
  modules: [
    {
      key: Modules.ORDER,
      options: {
        generateCustomDisplayId: async function (
          order: OrderTypes.CreateOrderDTO,
          sharedContext: Context
        ): Promise<string> {
          // Return your custom display ID
          return `${order.email}-${Date.now()}`
        },
      },
    },
    // other modules...
  ],
  // other configurations...
})
```

In the example above, the `generateCustomDisplayId` function generates a custom display ID by combining the order's email with the current timestamp.

You can implement any logic to generate a unique and meaningful display ID for your orders.

***

## View Custom Display ID in Medusa Admin

By default, Medusa Admin displays the `display_id` in the table on the Orders page. To view the custom display ID in the table, you can enable the `view_configurations` experimental feature.

To enable this feature, add the following to `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // other configurations...
  featureFlags: {
    view_configurations: true,
  },
})
```

This enables the feature's flag.

Next, run the necessary migrations:

```bash
npx medusa db:migrate
```

Then, start the Medusa application:

```bash npm2yarn
npm run dev
```

Finally, customize the Order view in Medusa Admin to display the `custom_display_id` property.


# Draft Orders Plugin

In this guide, you'll learn about the Draft Orders Plugin and its features.

The Draft Orders Plugin was initially only available to Cloud users during its early access phase. It is now available for all Medusa users.

## What is the Draft Orders Plugin?

The Draft Orders Plugin is a Medusa plugin that allows admin users to create and manage draft orders on behalf of customers from the Medusa Admin.

The Medusa application already has the foundation to support draft orders, including data models and API routes. This plugin adds a user interface for managing draft orders in the Medusa Admin dashboard.

The Draft Orders Plugin is especially useful for handling customer support scenarios or when a customer places an order offline, such as over the phone or in-store.

### Features

- [Create draft orders from the Medusa Admin.](https://docs.medusajs.com/user-guide/orders/draft-orders/create/index.html.md)
- [Manage items in a draft order, allowing admin users to add, update, or remove items](https://docs.medusajs.com/user-guide/orders/draft-orders/manage#manage-draft-orders-items/index.html.md).
- [Add shipping methods to draft orders.](https://docs.medusajs.com/user-guide/orders/draft-orders/manage#manage-draft-orders-shipping-methods/index.html.md).
- [Associate existing customers with draft orders.](https://docs.medusajs.com/user-guide/orders/draft-orders/manage#manage-draft-orders-customer-details/index.html.md).
- [Convert draft orders to regular orders.](https://docs.medusajs.com/user-guide/orders/draft-orders/manage#convert-draft-order-to-regular-order/index.html.md).

***

## Install the Draft Orders Plugin

The Draft Orders Plugin is available in all Medusa applications starting v2.10.0.

For earlier versions, you can install the plugin manually.

### Prerequisites

- [Medusa application >= v2.4.0](https://docs.medusajs.com/docs/learn/installation/index.html.md)

To install the Draft Orders Plugin:

1. Run the following command in your Medusa application's directory:

```bash npm2yarn
npm install @medusajs/draft-order
```

2. Add the plugin to your `medusa-config.ts` file:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  plugins: [
    {
      resolve: "@medusajs/draft-order",
      options: {},
    },
  ],
})
```

### Test the Draft Orders Plugin

To test the Draft Orders Plugin, start the Medusa application with the following command:

```bash npm2yarn
npm run dev
```

If you open the Medusa Admin at `localhost:9000/app` and log in, you'll find an "Orders -> Drafts" sidebar item.

***

## Draft Orders User Guides

To learn how to use the draft order features in the Medusa Admin, refer to the [Draft Orders](https://docs.medusajs.com/user-guide/orders/draft-orders/index.html.md) user guides.

***

## How Draft Orders Work

A draft order is stored in the database as a regular order. It is represented by the [Order data model](https://docs.medusajs.com/references/order/models/Order/index.html.md) with the following properties:

- `status`: Set to `draft` to indicate that the order is a draft.
- `is_draft_order`: Set to `true` to indicate that the order is a draft order.

So, the same [order concepts](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/concepts/index.html.md) apply to draft orders as well.

### Editing Draft Orders

Similar to regular orders, draft orders can be [edited](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md), allowing admin users to add, update, or remove items, as well as add shipping methods.

When the order edit is confirmed on the draft order, the changes are applied directly to the draft order and its [version](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-versioning/index.html.md) is incremented.

### Draft Order Locale

A draft order's `locale` can be set and updated before converting it to a regular order. All item details in the draft order, such as product titles and descriptions, will be presented in the correct language if translations are available.

When the draft order is converted to a regular order, it retains its `locale` property. The details of the order items will remain in the specified locale.

### Converting Draft Orders to Regular Orders

Once a draft order is finalized and ready for processing, it can be converted to a regular order. This involves:

- Changing its `status` property to `pending`.
- Changing its `is_draft_order` property to `false`.

Admin users can then manage the order like any other regular order, including processing payments and fulfilling items.


# Order Edit

In this guide, you'll learn about order edits.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/edit/index.html.md) to learn how to edit an order's items using the dashboard.

## What is an Order Edit?

A merchant can edit an order to add new items or change the quantity of existing items.

An order edit is represented by the [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md).

The `OrderChange` data model is associated with any type of change, including returns or exchanges. However, its `change_type` property distinguishes the type of change being made.

For order edits, the `OrderChange`'s `change_type` is `edit`.

***

## Add Items in an Order Edit

When a merchant adds new items to an order during editing, the item is added as an [OrderItem](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md).

Additionally, an `OrderChangeAction` is created. The [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md) represents a change made by an `OrderChange`, such as adding an item.

When an item is added, an `OrderChangeAction` is created with the type `ITEM_ADD`. Its `details` property stores the item's ID, price, and quantity.

***

## Update Items in an Order Edit

A merchant can update an existing item's quantity or price.

This change is recorded as an `OrderChangeAction` with the type `ITEM_UPDATE`. Its `details` property stores the item's ID, updated price, and updated quantity.

***

## Shipping Methods of New Items in the Edit

Adding new items to the order requires adding shipping methods for those items.

These shipping methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). Also, an `OrderChangeAction` is created with the type `SHIPPING_ADD`.

***

## How Order Edits Impact an Order’s Version

When an order edit is confirmed, the order’s [version](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-versioning/index.html.md) is incremented.

***

## Payments and Refunds for Order Edit Changes

Once the order edit is confirmed, any additional payment or refund required can be made on the original order.

This is determined by the comparison between the `OrderSummary` and the order's transactions, as mentioned in the [Transactions guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions#checking-outstanding-amount/index.html.md).


# Order Exchange

In this guide, you’ll learn about order exchanges.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/exchanges/index.html.md) to learn how to manage an order's exchanges using the dashboard.

## What is an Exchange?

An exchange is the replacement of an item that the customer ordered with another item.

A merchant creates the exchange, specifying which items to return and which new items to send.

The [OrderExchange data model](https://docs.medusajs.com/references/order/models/OrderExchange/index.html.md) represents an exchange.

***

## Returned and New Items

When an exchange is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is also created to handle receiving the items back from the customer.

Refer to the [Returns guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md) to learn more about returns and how they work.

The [OrderExchangeItem data model](https://docs.medusajs.com/references/order/models/OrderExchangeItem/index.html.md) represents the new items to be sent to the customer. It's associated with the `OrderExchange` data model.

***

## Exchange Shipping Methods

An exchange has shipping methods used to send the new items to the customer. They’re represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md).

The shipping methods for the returned items are associated with the exchange's return, as explained in the [Returns guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md).

***

## Exchange Payment

The `OrderExchange` data model has a `difference_due` property that stores the outstanding amount.

|Condition|Result|
|---|---|
|\`difference\_due \< 0\`|The merchant owes the customer a refund of the |
|\`difference\_due > 0\`|The merchant requires additional payment from the customer of the |
|\`difference\_due = 0\`|No payment processing is required.|

Any payments or refunds made are stored in the [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md).

***

## How Exchanges Impact an Order’s Version

When an exchange is confirmed, the order’s [version](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-versioning/index.html.md) is incremented.


# Links between Order Module and Other Modules

This document showcases the module links defined between the Order Module and other Commerce Modules.

## Summary

The Order Module has the following links to other modules:

Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|Order|Customer|Read-only - has one|Learn more|
|Order|Cart|Stored - one-to-one|Learn more|
|Order|Fulfillment|Stored - one-to-many|Learn more|
|Return|Fulfillment|Stored - one-to-many|Learn more|
|Order|PaymentCollection|Stored - one-to-many|Learn more|
|OrderClaim|PaymentCollection|Stored - one-to-many|Learn more|
|OrderExchange|PaymentCollection|Stored - one-to-many|Learn more|
|OrderLineItem|Product|Read-only - has many|Learn more|
|Order|Promotion|Stored - many-to-many|Learn more|
|Order|Region|Read-only - has one|Learn more|
|Order|SalesChannel|Read-only - has one|Learn more|

***

## Customer Module

Medusa defines a read-only link between the `Order` data model and the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md)'s `Customer` data model. This means you can retrieve the details of an order's customer, but you don't manage the links in a pivot table in the database. The customer of an order is determined by the `customer_id` property of the `Order` data model.

### Retrieve with Query

To retrieve the customer of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`:

### query.graph

```ts
const { data: orders } = await query.graph({
  entity: "order",
  fields: [
    "customer.*",
  ],
})

// orders[0].customer
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: orders } = useQueryGraphStep({
  entity: "order",
  fields: [
    "customer.*",
  ],
})

// orders[0].customer
```

***

## Cart Module

The [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md) provides cart-management features.

Medusa defines a link between the `Order` and `Cart` data models. The order is linked to the cart used for the purchased.

![A diagram showcasing an example of how data models from the Cart and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728375735/Medusa%20Resources/cart-order_ijwmfs.jpg)

### Retrieve with Query

To retrieve the cart of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `cart.*` in `fields`:

### query.graph

```ts
const { data: orders } = await query.graph({
  entity: "order",
  fields: [
    "cart.*",
  ],
})

// orders[0].cart
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: orders } = useQueryGraphStep({
  entity: "order",
  fields: [
    "cart.*",
  ],
})

// orders[0].cart
```

### Manage with Link

To manage the cart of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.CART]: {
    cart_id: "cart_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.CART]: {
    cart_id: "cart_123",
  },
})
```

***

## Fulfillment Module

A fulfillment is created for an orders' items. Medusa defines a link between the `Fulfillment` and `Order` data models.

![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716549903/Medusa%20Resources/order-fulfillment_h0vlps.jpg)

A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models.

![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399052/Medusa%20Resources/Social_Media_Graphics_2024_Order_Return_vetimk.jpg)

### Retrieve with Query

To retrieve the fulfillments of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `fulfillments.*` in `fields`:

To retrieve the fulfillments of a return, pass `fulfillments.*` in `fields`.

### query.graph

```ts
const { data: orders } = await query.graph({
  entity: "order",
  fields: [
    "fulfillments.*",
  ],
})

// orders[0].fulfillments
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: orders } = useQueryGraphStep({
  entity: "order",
  fields: [
    "fulfillments.*",
  ],
})

// orders[0].fulfillments
```

### Manage with Link

To manage the fulfillments of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.FULFILLMENT]: {
    fulfillment_id: "ful_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.FULFILLMENT]: {
    fulfillment_id: "ful_123",
  },
})
```

***

## Payment Module

An order's payment details are stored in a payment collection. This also applies for claims and exchanges.

So, Medusa defines links between the `PaymentCollection` data model and the `Order`, `OrderClaim`, and `OrderExchange` data models.

![A diagram showcasing an example of how data models from the Order and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716554726/Medusa%20Resources/order-payment_ubdwok.jpg)

### Retrieve with Query

To retrieve the payment collections of an order, order exchange, or order claim with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_collections.*` in `fields`:

### query.graph

```ts
const { data: orders } = await query.graph({
  entity: "order",
  fields: [
    "payment_collections.*",
  ],
})

// orders[0].payment_collections
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: orders } = useQueryGraphStep({
  entity: "order",
  fields: [
    "payment_collections.*",
  ],
})

// orders[0].payment_collections
```

### Manage with Link

To manage the payment collections of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.PAYMENT]: {
    payment_collection_id: "paycol_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.PAYMENT]: {
    payment_collection_id: "paycol_123",
  },
})
```

***

## Product Module

Medusa defines read-only links between:

- the `OrderLineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `Product` data model. This means you can retrieve the details of a line item's product, but you don't manage the links in a pivot table in the database. The product of a line item is determined by the `product_id` property of the `OrderLineItem` data model.
- the `OrderLineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `ProductVariant` data model. This means you can retrieve the details of a line item's variant, but you don't manage the links in a pivot table in the database. The variant of a line item is determined by the `variant_id` property of the `OrderLineItem` data model.

### Retrieve with Query

To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`:

To retrieve the product, pass `product.*` in `fields`.

### query.graph

```ts
const { data: lineItems } = await query.graph({
  entity: "order_line_item",
  fields: [
    "variant.*",
  ],
})

// lineItems.variant
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: lineItems } = useQueryGraphStep({
  entity: "order_line_item",
  fields: [
    "variant.*",
  ],
})

// lineItems.variant
```

***

## Promotion Module

An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models.

![A diagram showcasing an example of how data models from the Order and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716555015/Medusa%20Resources/order-promotion_dgjzzd.jpg)

### Retrieve with Query

To retrieve the promotion applied on an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `promotion.*` in `fields`:

### query.graph

```ts
const { data: orders } = await query.graph({
  entity: "order",
  fields: [
    "promotions.*",
  ],
})

// orders[0].promotions
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: orders } = useQueryGraphStep({
  entity: "order",
  fields: [
    "promotions.*",
  ],
})

// orders[0].promotions
```

### Manage with Link

To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.PROMOTION]: {
    promotion_id: "promo_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.PROMOTION]: {
    promotion_id: "promo_123",
  },
})
```

***

## Region Module

Medusa defines a read-only link between the `Order` data model and the [Region Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md)'s `Region` data model. This means you can retrieve the details of an order's region, but you don't manage the links in a pivot table in the database. The region of an order is determined by the `region_id` property of the `Order` data model.

### Retrieve with Query

To retrieve the region of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`:

### query.graph

```ts
const { data: orders } = await query.graph({
  entity: "order",
  fields: [
    "region.*",
  ],
})

// orders[0].region
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: orders } = useQueryGraphStep({
  entity: "order",
  fields: [
    "region.*",
  ],
})

// orders[0].region
```

***

## Sales Channel Module

Medusa defines a read-only link between the `Order` data model and the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md)'s `SalesChannel` data model. This means you can retrieve the details of an order's sales channel, but you don't manage the links in a pivot table in the database. The sales channel of an order is determined by the `sales_channel_id` property of the `Order` data model.

### Retrieve with Query

To retrieve the sales channel of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`:

### query.graph

```ts
const { data: orders } = await query.graph({
  entity: "order",
  fields: [
    "sales_channel.*",
  ],
})

// orders[0].sales_channel
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: orders } = useQueryGraphStep({
  entity: "order",
  fields: [
    "sales_channel.*",
  ],
})

// orders[0].sales_channel
```


# Order Change

In this document, you'll learn what an order change is and the related concepts.

## OrderChange Data Model

The [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md) represents any kind of change to an order, such as a [return](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md), [exchange](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md), or [edit](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md). Each of these is essentially an order change that is confirmed to apply changes to the original order.

The `OrderChange` model's `change_type` property indicates the purpose of the order change:

1. `edit`: The order change is making edits to the order, as explained in the [Order Edit](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md) guide.
2. `exchange`: The order change is associated with an exchange, which you can learn about in the [Order Exchange](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md) guide.
3. `claim`: The order change is associated with a claim, which you can learn about in the [Order Claim](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md) guide.
4. `return_request` or `return_receive`: The order change is associated with a return, which you can learn about in the [Order Return](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md) guide.

Once the order change is confirmed, its changes are applied to the order.

***

## Order Change Actions

The actions performed on the original order by an order change, such as adding an item, are represented by the [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md).

The `OrderChangeAction` has an `action` property that indicates the type of action to perform on the order, and a `details` property that holds additional information related to the action.

The following table lists the possible `action` values that Medusa uses and the corresponding `details` they carry.

|Action|Description|Details|
|---|---|---|---|---|
|\`ITEM\_ADD\`|Add an item to the order. The item is only added after the change is confirmed.|\`details\`|
|\`ITEM\_UPDATE\`|Update an item in an order change. It's only applied to the order after the change is confirmed.|\`details\`|
|\`ITEM\_REMOVE\`|Remove an item from an order change. This can happen when a claim or an exchange is canceled.|\`details\`|
|\`RETURN\_ITEM\`|Set an item to be returned.|\`details\`|
|\`RECEIVE\_RETURN\_ITEM\`|Mark a return item as received.|\`details\`|
|\`RECEIVE\_DAMAGED\_RETURN\_ITEM\`|Mark a return item that's damaged as received.|\`details\`|
|\`CANCEL\_RETURN\_ITEM\`|Cancel the return of an item. This can happen when a return is canceled.|\`details\`|
|\`SHIPPING\_ADD\`|Add a shipping method to an order change. It's only added to the order after the change is confirmed.|No details added. The ID to the shipping method is added in the |
|\`SHIPPING\_REMOVE\`|Remove a shipping method from an order change. This can happen when a claim or an exchange is canceled.|No details added. The ID to the shipping method is added in the |
|\`SHIP\_ITEM\`|Mark an item's quantity as shipped.|\`details\`|
|\`FULFILL\_ITEM\`|Fulfill an item's quantity as part of a change.|\`details\`|
|\`DELIVER\_ITEM\`|Mark an item's quantity as delivered.|\`details\`|
|\`WRITE\_OFF\_ITEM\`|Remove an item's quantity from an order change, without adding the quantity back to the item variant's inventory. The quantity isn't removed from the order until the change is confirmed.|\`details\`|
|\`REINSTATE\_ITEM\`|Reinstate an item's quantity in an order change that was previously written off. The quantity is added back to the item variant's inventory when the change is confirmed.|\`details\`|
|\`TRANSFER\_CUSTOMER\`|Transfer an order to another customer. The order is not removed from the original customer until the change is confirmed.|No details added. The ID to the new customer is added in the |
|\`UPDATE\_ORDER\_PROPERTIES\`|Update the properties of an order, such as customer information or shipping address. The properties are not updated on the original order until the change is confirmed.|\`details\`|
|\`CREDIT\_LINE\_ADD\`|Add a credit line to an order. The credit line is not added to the original order until the change is confirmed.|No details added. The ID to the associated payment is added in the |


# Retrieve Order Totals Using Query

In this guide, you'll learn how to retrieve order totals in your Medusa application using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).

You may need to retrieve order totals in your Medusa customizations, such as workflows or custom API routes, to perform custom actions with them. The ideal way to retrieve totals is using Query.

Refer to the [Order Confirmation in Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/order-confirmation#show-order-totals/index.html.md) guide for a storefront-specific approach.

## How to Retrieve Order Totals with Query

To retrieve order totals, you mainly need to pass the `total` field within the `fields` option of the Query. This will return the order's grand total, along with the totals of its line items and shipping methods. You can also pass additional total fields that you need for your use case.

For example, to retrieve all totals of an order:

Use `useQueryGraphStep` in workflows, and `query.graph` in custom API routes, scheduled jobs, and subscribers.

### useQueryGraphStep

```ts highlights={[["12"]]}
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

export const myWorkflow = createWorkflow(
  "my-workflow",
  () => {
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "id",
        "currency_code",
        "total",
        "subtotal",
        "tax_total",
        "original_total",
        "original_subtotal",
        "original_tax_total",
        "discount_total",
        "discount_tax_total",
        "shipping_total",
        "shipping_subtotal",
        "shipping_tax_total",
        "original_shipping_total",
        "original_shipping_subtotal",
        "original_shipping_tax_total",
        "item_total",
        "item_tax_total",
        "item_subtotal",
        "original_item_total",
        "original_item_tax_total",
        "original_item_subtotal",
        "gift_card_total",
        "gift_card_tax_total",
        "items.*",
        "shipping_methods.*",
        "summary.*",
      ],
      filters: {
        id: "order_123", // Specify the order ID
      },
    })
  }
)
```

### query.graph

```ts highlights={[["8"]]}
const query = container.resolve("query") // or req.scope.resolve in API routes

const { data: [order] } = await query.graph({
  entity: "order",
  fields: [
    "id",
    "currency_code",
    "total",
    "subtotal",
    "tax_total",
    "original_total",
    "original_subtotal",
    "original_tax_total",
    "discount_total",
    "discount_tax_total",
    "shipping_total",
    "shipping_subtotal",
    "shipping_tax_total",
    "original_shipping_total",
    "original_shipping_subtotal",
    "original_shipping_tax_total",
    "item_total",
    "item_tax_total",
    "item_subtotal",
    "original_item_total",
    "original_item_tax_total",
    "original_item_subtotal",
    "gift_card_total",
    "gift_card_tax_total",
    "items.*",
    "shipping_methods.*",
    "summary.*",
  ],
  filters: {
    id: "order_123", // Specify the order ID
  },
})
```

The returned `order` object will look like this:

```json
{
  "id": "order_01K1GEZ6Y1V9651AJNYG1WV3TC",
  "currency_code": "eur",
  "total": 20,
  "subtotal": 20,
  "tax_total": 0,
  "original_total": 20,
  "original_tax_total": 0,
  "discount_total": 0,
  "discount_tax_total": 0,
  "shipping_total": 10,
  "shipping_subtotal": 10,
  "shipping_tax_total": 0,
  "original_shipping_total": 10,
  "original_shipping_subtotal": 10,
  "original_shipping_tax_total": 0,
  "item_total": 10,
  "item_tax_total": 0,
  "item_subtotal": 10,
  "original_item_total": 10,
  "original_item_tax_total": 0,
  "original_item_subtotal": 10,
  "items": [
    {
      "subtotal": 10,
      "total": 10,
      "original_total": 10,
      "discount_total": 0,
      "discount_subtotal": 0,
      "discount_tax_total": 0,
      "tax_total": 0,
      "original_tax_total": 0,
      "refundable_total_per_unit": 10,
      "refundable_total": 10,
      "fulfilled_total": 0,
      "shipped_total": 0,
      "return_requested_total": 0,
      "return_received_total": 0,
      "return_dismissed_total": 0,
      "write_off_total": 0,
      // ...
    }
  ],
  "shipping_methods": [
    {
      "subtotal": 10,
      "total": 10,
      "original_total": 10,
      "discount_total": 0,
      "discount_subtotal": 0,
      "discount_tax_total": 0,
      "tax_total": 0,
      "original_tax_total": 0,
      // ...
    }
  ],
  "summary": {
    "paid_total": 0,
    "refunded_total": 0,
    "accounting_total": 20,
    "credit_line_total": 0,
    "transaction_total": 0,
    "pending_difference": 20,
    "current_order_total": 20,
    "original_order_total": 20
  }
}
```

### Order Totals

The order will include the following total fields:

- `total`: The order's final total after discounts and credit lines, including taxes.
- `subtotal`: The order's subtotal before discounts, excluding taxes. Calculated as the sum of `item_subtotal` and `shipping_subtotal`.
- `tax_total`: The order's tax total after discounts. Calculated as the sum of `item_tax_total` and `shipping_tax_total`.
- `original_total`: The order's total before discounts, including taxes. Calculated as the sum of `original_item_total` and `original_shipping_total`.
- `original_subtotal`: The order's subtotal before discounts, excluding taxes. Calculated as the sum of `original_item_subtotal` and `original_shipping_subtotal`.
- `original_tax_total`: The order's tax total before discounts. Calculated as the sum of `original_item_tax_total` and `original_shipping_tax_total`.
- `discount_total`: The total amount of discounts applied to the order, including the tax portion of discounts.
- `discount_tax_total`: The total amount of discounts applied to the order's tax. Represents the tax portion of discounts.
- `shipping_total`: The sum of all shipping methods' totals after discounts, including taxes.
- `shipping_subtotal`: The sum of all shipping methods' subtotals before discounts, excluding taxes.
- `shipping_tax_total`: The sum of all shipping methods' tax totals after discounts.
- `original_shipping_total`: The sum of all shipping methods' original totals before discounts, including taxes.
- `original_shipping_subtotal`: The sum of all shipping methods' original subtotals before discounts, excluding taxes.
- `original_shipping_tax_total`: The sum of all shipping methods' original tax totals before discounts.
- `item_total`: The sum of all line items' totals after discounts, including taxes.
- `item_tax_total`: The sum of all line items' tax totals after discounts.
- `item_subtotal`: The sum of all line items' subtotals before discounts, excluding taxes.
- `original_item_total`: The sum of all line items' original totals before discounts, including taxes.
- `original_item_tax_total`: The sum of all line items' original tax totals before discounts.
- `original_item_subtotal`: The sum of all line items' original subtotals before discounts, excluding taxes.
- `gift_card_total`: The total amount of gift cards applied to the order.
- `gift_card_tax_total`: The tax total of the gift cards applied to the order.

### Order Line Item Totals

The `items` array in the `order` object contains total fields for each line item:

- `unit_price`: The price of a single unit of the line item. This field is not calculated and is stored in the database.
- `subtotal`: The line item's subtotal before discounts, excluding taxes.
- `total`: The line item's total after discounts, including taxes.
- `original_total`: The line item's original total before discounts, including taxes.
- `discount_total`: The total amount of discounts applied to the line item, including the tax portion of discounts.
- `discount_subtotal`: The total amount of discounts applied to the line item's subtotal (excluding tax portion).
- `discount_tax_total`: The total amount of discounts applied to the line item's tax. Represents the tax portion of discounts.
- `tax_total`: The line item's tax total after discounts.
- `original_tax_total`: The line item's original tax total before discounts.
- `refundable_total_per_unit`: The total amount that can be refunded per unit of the line item.
- `refundable_total`: The total amount that can be refunded for the line item.
- `fulfilled_total`: The total amount of the line item that has been fulfilled.
- `shipped_total`: The total amount of the line item that has been shipped.
- `return_requested_total`: The total amount of the line item that has been requested for return.
- `return_received_total`: The total amount of the line item that has been received from a return.
- `return_dismissed_total`: The total amount of the line item that has been dismissed by a return.
  - These items are considered damaged and are not returned to the inventory.
- `write_off_total`: The total amount of the line item that has been written off.
  - For example, if the order is edited and the line item is removed or its quantity has changed.

### Order Shipping Method Totals

The `shipping_methods` array in the `order` object contains total fields for each shipping method:

- `amount`: The amount charged for the shipping method. This field is not calculated and is stored in the database.
- `subtotal`: The shipping method's subtotal before discounts, excluding taxes.
- `total`: The shipping method's total after discounts, including taxes.
- `original_total`: The shipping method's original total before discounts, including taxes.
- `discount_total`: The total amount of discounts applied to the shipping method, including the tax portion of discounts.
- `discount_subtotal`: The total amount of discounts applied to the shipping method's subtotal (excluding tax portion).
- `discount_tax_total`: The total amount of discounts applied to the shipping method's tax. Represents the tax portion of discounts.
- `tax_total`: The shipping method's tax total after discounts.
- `original_tax_total`: The shipping method's original tax total before discounts.

### Order Summary Totals

The `summary` object in the `order` object provides an overview of the order's transactions. It includes the following fields:

- `paid_total`: The total amount that has been paid for the order.
- `refunded_total`: The total amount that has been refunded for the order.
- `accounting_total`: The order's total without the credit-line total.
- `credit_line_total`: The total amount of credit lines applied to the order.
- `transaction_total`: The total amount of transactions associated with the order.
- `pending_difference`: The difference between the order's total and the paid total.
- `current_order_total`: The current total of the order. It's useful if the order has been edited or modified.
- `original_order_total`: The original total of the order before any modifications.

***

## Caveats of Retrieving Order Totals

### Using Asterisk (\*) in Order Query

Order totals are calculated based on the order's line items, shipping methods, taxes, and any discounts applied. They are not stored in the `Order` data model.

For that reason, you cannot retrieve order totals by passing `*` to the Query's fields. For example, the following query will not return order totals:

### useQueryGraphStep

```ts
const { data: orders } = useQueryGraphStep({
  entity: "order",
  fields: ["*"],
  filters: {
    id: "order_123",
  },
})

// orders don't include order totals
```

### query.graph

```ts
const { data: [order] } = await query.graph({
  entity: "order",
  fields: ["*"],
  filters: {
    id: "order_123",
  },
})

// order doesn't include order totals
```

This will return the order data stored in the database, but not the calculated totals.

You also can't pass `*` along with `total` in the Query's fields option. Passing `*` will override the `total` field, and you will not retrieve order totals. For example, the following query will not return order totals:

### useQueryGraphStep

```ts
const { data: orders } = useQueryGraphStep({
  entity: "order",
  fields: ["*", "total"],
  filters: {
    id: "order_123",
  },
})

// orders don't include order totals
```

### query.graph

```ts
const { data: [order] } = await query.graph({
  entity: "order",
  fields: ["*", "total"],
  filters: {
    id: "order_123",
  },
})

// order doesn't include order totals
```

Instead, when you need to retrieve order totals, explicitly pass the `total` field in the Query's fields option, as shown in [the previous section](#how-to-retrieve-order-totals-with-query). You can also include other fields and relations you need, such as `email` and `items.*`.

### Applying Filters on Order Totals

You can't apply filters directly on the order totals, as they are not stored in the `Order` data model. You can still filter the order based on its properties, such as `id`, `email`, or `currency_code`, but not on the calculated totals.


# Order Versioning

In this document, you’ll learn how orders and their details are versioned.

## What is Order Versioning?

Versioning means assigning a version number to a record, such as an order and its items. This is useful for viewing the different versions of an order following changes throughout its lifetime.

When changes are confirmed on an order, such as when an item is added or returned, the order's version changes.

***

## version Property

The `Order` and `OrderSummary` data models have a `version` property that indicates the current version. By default, its value is `1`.

Other order-related data models, such as `OrderItem`, also have a `version` property, but it indicates the version to which the record belongs.

***

## How the Version Changes

When the order is changed, such as when an item is exchanged, Medusa updates the order's version and its related data:

1. The version of the order and its summary is incremented.
2. Related order data that have a `version` property, such as `OrderItem`, are duplicated. The duplicated item has the new version, while the original item retains the previous version.

When the order is retrieved, only the related data with the same version is returned.


# Order Module

In this section of the documentation, you will find resources to learn more about the Order Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/index.html.md) to learn how to manage orders using the dashboard.

Medusa has order related features available out-of-the-box through the Order Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Order Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Order Features

- [Order Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/concepts/index.html.md): Store and manage your orders to retrieve, create, cancel, and perform other operations.
- [Draft Orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/draft-orders/index.html.md): Allow merchants to create orders on behalf of their customers as draft orders that later are transformed to regular orders.
- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/promotion-adjustments/index.html.md): Apply promotions or discounts to the order's items and shipping methods by adding adjustment lines that are factored into their subtotals.
- [Apply Tax Lines](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/tax-lines/index.html.md): Apply tax lines to an order's line items and shipping methods.
- [Returns](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md), [Edits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md), [Exchanges](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md), and [Claims](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md): Make [changes](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) to an order to edit, return, or exchange its items, with [version-based control](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-versioning/index.html.md) over the order's timeline.

***

## How to Use the Order Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-draft-order.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createDraftOrderStep = createStep(
  "create-order",
  async ({}, { container }) => {
    const orderModuleService = container.resolve(Modules.ORDER)

    const draftOrder = await orderModuleService.createOrders({
      currency_code: "usd",
      items: [
        {
          title: "Shirt",
          quantity: 1,
          unit_price: 3000,
        },
      ],
      shipping_methods: [
        {
          name: "Express shipping",
          amount: 3000,
        },
      ],
      status: "draft",
    })

    return new StepResponse({ draftOrder }, draftOrder.id)
  },
  async (draftOrderId, { container }) => {
    if (!draftOrderId) {
      return
    }
    const orderModuleService = container.resolve(Modules.ORDER)

    await orderModuleService.deleteOrders([draftOrderId])
  }
)

export const createDraftOrderWorkflow = createWorkflow(
  "create-draft-order",
  () => {
    const { draftOrder } = createDraftOrderStep()

    return new WorkflowResponse({
      draftOrder,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createDraftOrderWorkflow } from "../../workflows/create-draft-order"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createDraftOrderWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createDraftOrderWorkflow } from "../workflows/create-draft-order"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createDraftOrderWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createDraftOrderWorkflow } from "../workflows/create-draft-order"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createDraftOrderWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Promotions Adjustments in Orders

In this document, you’ll learn how a promotion is applied to an order’s items and shipping methods using adjustment lines.

## What are Adjustment Lines?

An adjustment line indicates a change to a line item or a shipping method’s amount. It’s used to apply promotions or discounts on an order.

The [OrderLineItemAdjustment data model](https://docs.medusajs.com/references/order/models/OrderLineItemAdjustment/index.html.md) represents changes on a line item, and the [OrderShippingMethodAdjustment data model](https://docs.medusajs.com/references/order/models/OrderShippingMethodAdjustment/index.html.md) represents changes on a shipping method.

![A diagram showcasing the relation between an order, its items and shipping methods, and their adjustment lines](https://res.cloudinary.com/dza7lstvk/image/upload/v1712306017/Medusa%20Resources/order-adjustments_myflir.jpg)

The `amount` property of the adjustment line indicates the amount to be discounted from the original amount.

The ID of the applied promotion is stored in the `promotion_id` property of the adjustment line.

***

## Discountable Option

The `OrderLineItem` data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default.

When disabled, a promotion can’t be applied to a line item. In the context of the Promotion Module, the promotion isn’t applied to the line item even if it matches its rules.

***

## Promotion Actions

When using the Order and Promotion modules together, use the [computeActions method of the Promotion Module’s main service](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). It retrieves the actions of line items and shipping methods.

Learn more about actions in the [Promotion Module’s documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md).

```ts collapsibleLines="1-10" expandButtonLabel="Show Imports"
import {
  ComputeActionAdjustmentLine,
  ComputeActionItemLine,
  ComputeActionShippingLine,
  // ...
} from "@medusajs/framework/types"

// ...

// retrieve the order
const order = await orderModuleService.retrieveOrder("ord_123", {
  relations: [
    "items.item.adjustments",
    "shipping_methods.shipping_method.adjustments",
  ],
})
// retrieve the line item adjustments
const lineItemAdjustments: ComputeActionItemLine[] = []
order.items.forEach((item) => {
  const filteredAdjustments = item.adjustments?.filter(
    (adjustment) => adjustment.code !== undefined
  ) as unknown as ComputeActionAdjustmentLine[]
  if (filteredAdjustments.length) {
    lineItemAdjustments.push({
      ...item,
      ...item.detail,
      adjustments: filteredAdjustments,
    })
  }
})

//retrieve shipping method adjustments
const shippingMethodAdjustments: ComputeActionShippingLine[] =
  []
order.shipping_methods.forEach((shippingMethod) => {
  const filteredAdjustments =
    shippingMethod.adjustments?.filter(
      (adjustment) => adjustment.code !== undefined
    ) as unknown as ComputeActionAdjustmentLine[]
  if (filteredAdjustments.length) {
    shippingMethodAdjustments.push({
      ...shippingMethod,
      adjustments: filteredAdjustments,
    })
  }
})

// compute actions
const actions = await promotionModuleService.computeActions(
  ["promo_123"],
  {
    items: lineItemAdjustments,
    shipping_methods: shippingMethodAdjustments,
    // TODO infer from cart or region
    currency_code: "usd",
  }
)
```

The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately.

Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the order’s line items and the shipping method’s adjustments.

```ts collapsibleLines="1-9" expandButtonLabel="Show Imports"
import {
  AddItemAdjustmentAction,
  AddShippingMethodAdjustment,
  // ...
} from "@medusajs/framework/types"

// ...

await orderModuleService.setOrderLineItemAdjustments(
  order.id,
  actions.filter(
    (action) => action.action === "addItemAdjustment"
  ) as AddItemAdjustmentAction[]
)

await orderModuleService.setOrderShippingMethodAdjustments(
  order.id,
  actions.filter(
    (action) =>
      action.action === "addShippingMethodAdjustment"
  ) as AddShippingMethodAdjustment[]
)
```


# Order Return

In this document, you’ll learn about order returns.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/returns/index.html.md) to learn how to manage an order's returns using the dashboard.

## What is a Return?

A return is the return of items delivered from the customer back to the merchant. It is represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md).

A return is requested either by the customer from the storefront, or the merchant from the admin. Medusa supports an automated Return Merchandise Authorization (RMA) flow.

![Diagram showcasing the automated RMA flow.](https://res.cloudinary.com/dza7lstvk/image/upload/v1719578128/Medusa%20Resources/return-rma_pzprwq.jpg)

Once the merchant receives the returned items, they mark the return as received.

***

## Returned Items

The items to be returned are represented by the [ReturnItem data model](https://docs.medusajs.com/references/order/models/ReturnItem/index.html.md).

The `ReturnItem` model has two properties storing the item's quantity:

1. `received_quantity`: The quantity of the item that's received and can be added to the item's inventory quantity.
2. `damaged_quantity`: The quantity of the item that's damaged, meaning it can't be sold again or added to the item's inventory quantity.

***

## Return Shipping Methods

A return has shipping methods used to return the items to the merchant. The shipping methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md).

In the Medusa application, the shipping method for a return is created only from a shipping option, provided by the Fulfillment Module, that has the rule `is_return` enabled.

***

## Refund Payment

The `refund_amount` property of the `Return` data model holds the amount a merchant must refund the customer.

The [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the return.

***

## Returns in Exchanges and Claims

When a merchant creates an exchange or a claim, it includes returning items from the customer.

The `Return` data model also represents the return of these items. In this case, the return is associated with the exchange or claim it was created for.

***

## How Returns Impact an Order’s Version

The order’s version is incremented when:

1. A return is requested.
2. A return is marked as received.


# Tax Lines in Order Module

In this document, you’ll learn about tax lines in an order.

## What are Tax Lines?

A tax line indicates the tax rate of a line item or a shipping method.

The [OrderLineItemTaxLine data model](https://docs.medusajs.com/references/order/models/OrderLineItemTaxLine/index.html.md) represents a line item’s tax line, and the [OrderShippingMethodTaxLine data model](https://docs.medusajs.com/references/order/models/OrderShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line.

![A diagram showcasing the relation between orders, items and shipping methods, and tax lines](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307225/Medusa%20Resources/order-tax-lines_sixujd.jpg)

***

## Tax Inclusivity

By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount and then adding it to the item/method’s subtotal.

However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes.

So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal.

The following diagram is a simplified showcase of how a subtotal is calculated from the tax perspective.

![A diagram showcasing how a subtotal is calculated from the tax perspective](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307395/Medusa%20Resources/order-tax-inclusive_oebdnm.jpg)

For example, if a line item's amount is `5000`, the tax rate is `10`, and `is_tax_inclusive` is enabled, the tax amount is 10% of `5000`, which is `500`. The item's unit price becomes `4500`.


# Transactions

In this guide, you’ll learn about order transactions, how to check an order’s outstanding amount, and how transactions relate to payments and refunds.

## What is a Transaction?

A transaction represents any order payment process, such as capturing or refunding an amount. It’s represented by the [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md).

The transaction’s main purpose is to ensure a correct balance between paid and outstanding amounts.

Transactions are also associated with returns, claims, and exchanges when additional payment or refunds are required.

***

## Checking Outstanding Amount

The order’s total amounts are stored in the `OrderSummary`'s `totals` property, which is a JSON object holding the total details of the order.

```json
{
  "totals": {
    "total": 30,
    "subtotal": 30,
    // ...
  }
}
```

To check the outstanding amount of an order, the transaction amounts from the `Transaction` records are summed. Then, the following conditions are evaluated:

|Condition|Result|
|---|---|---|
|summary’s total - transaction amounts total = 0|There’s no outstanding amount.|
|summary’s total - transaction amounts total > 0|The customer owes additional payment to the merchant.|
|summary’s total - transaction amounts total \< 0|The merchant owes the customer a refund.|

***

## Transaction Reference

The Order Module doesn’t provide payment processing functionalities, so it doesn’t store payments that can be processed. Payment functionalities are provided by the [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md).

The `OrderTransaction` data model has two properties that determine which data model and record holds the actual payment’s details:

- `reference`: indicates the table’s name in the database. For example, `capture` from the [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md).
- `reference_id`: indicates the ID of the record in the table. For example, `capt_123`.

### Refund Transactions

When a refund is created for an order, an `OrderTransaction` record is created with its `reference` property set to `refund`. The `reference_id` property is set to the ID of the refund record in the [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md).

The transaction's `amount` for refunds will be a negative value, indicating that the merchant owes the customer a refund.

#### Example: Retrieve Order's Refunds

To retrieve an order's refunds, you can retrieve the order's transactions using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) and filter the transactions by their `reference` property.

For example:

```ts
const query = container.resolve("query") // or req.scope.resolve("query")

const { data: [order] } = await query.graph({
  entity: "order",
  fields: ["*", "transactions.*"],
  filters: {
    id: "order_123",
  },
})

const refundTransactions = order.transactions?.filter(
  (t) => t?.reference === "refund"
)

// calculate total refunded amount as a positive value
const totalRefunded = refundTransactions?.reduce((acc, t) => acc + Math.abs(t!.amount || 0), 0)
```


# Commerce Modules

In this section of the documentation, you'll find guides and references related to Medusa's Commerce Modules.

A Commerce Module provides features for a commerce domain within its service. When you install the Medusa application, all Commerce Modules are available out-of-the-box. The Medusa application exposes their features in its core API routes to clients.

A Commerce Module also defines data models, representing tables in the database. The Medusa Framework and tools allow you to extend these data models to add custom fields.

## Commerce Modules List

- [API Key Module](https://docs.medusajs.com/commerce-modules/api-key/index.html.md)
- [Auth Module](https://docs.medusajs.com/commerce-modules/auth/index.html.md)
- [Cart Module](https://docs.medusajs.com/commerce-modules/cart/index.html.md)
- [Currency Module](https://docs.medusajs.com/commerce-modules/currency/index.html.md)
- [Customer Module](https://docs.medusajs.com/commerce-modules/customer/index.html.md)
- [Fulfillment Module](https://docs.medusajs.com/commerce-modules/fulfillment/index.html.md)
- [Inventory Module](https://docs.medusajs.com/commerce-modules/inventory/index.html.md)
- [Order Module](https://docs.medusajs.com/commerce-modules/order/index.html.md)
- [Payment Module](https://docs.medusajs.com/commerce-modules/payment/index.html.md)
- [Pricing Module](https://docs.medusajs.com/commerce-modules/pricing/index.html.md)
- [Product Module](https://docs.medusajs.com/commerce-modules/product/index.html.md)
- [Promotion Module](https://docs.medusajs.com/commerce-modules/promotion/index.html.md)
- [Region Module](https://docs.medusajs.com/commerce-modules/region/index.html.md)
- [Sales Channel Module](https://docs.medusajs.com/commerce-modules/sales-channel/index.html.md)
- [Stock Location Module](https://docs.medusajs.com/commerce-modules/stock-location/index.html.md)
- [Store Module](https://docs.medusajs.com/commerce-modules/store/index.html.md)
- [Tax Module](https://docs.medusajs.com/commerce-modules/tax/index.html.md)
- [Translation Module](https://docs.medusajs.com/commerce-modules/translation/index.html.md)

[User Module](https://docs.medusajs.com/commerce-modules/user/index.html.md): undefined

***

## How to Use Modules

The Commerce Modules can be used in many use cases, including:

- Medusa Application: The Medusa application comes with all Commerce Modules pre-installed and configured. Their commerce features are exposed through REST APIs.
- Serverless Application: Use the Commerce Modules in a serverless application, such as a Next.js application, without having to manage a fully-fledged ecommerce system. You can use it by installing it in your Node.js project as an NPM package.
- Node.js Application: Use the Commerce Modules in any Node.js application by installing it with NPM.


# Account Holders and Saved Payment Methods

In this documentation, you'll learn about account holders, and how they're used to save payment methods in third-party payment providers.

Account holders are available starting from Medusa `v2.5.0`.

## What's an Account Holder?

An account holder represents a customer that can have saved payment methods in a third-party service. It's represented by the `AccountHolder` data model.

It holds fields retrieved from the third-party provider, such as:

- `external_id`: The ID of the equivalent customer or account holder in the third-party provider.
- `data`: Data returned by the payment provider when the account holder is created.

A payment provider that supports saving payment methods for customers would create the equivalent of an account holder in the third-party provider. Then, whenever a payment method is saved, it would be saved under the account holder in the third-party provider.

### Relation between Account Holder and Customer

The Medusa application creates a link between the [Customer](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) data model of the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md) and the `AccountHolder` data model of the Payment Module.

This link indicates that a customer can have more than one account holder, each representing saved payment methods in different payment providers.

Learn more about this link in the [Link to Other Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/links-to-other-modules/index.html.md) guide.

***

## Save Payment Methods

If a payment provider supports saving payment methods for a customer, they must implement the following methods:

- `createAccountHolder`: Creates an account holder in the payment provider. The Payment Module uses this method before creating the account holder in Medusa, and uses the returned data to set fields like `external_id` and `data` in the created `AccountHolder` record.
- `deleteAccountHolder`: Deletes an account holder in the payment provider. The Payment Module uses this method when an account holder is deleted in Medusa.
- `savePaymentMethod`: Saves a payment method for an account holder in the payment provider.
- `listPaymentMethods`: Lists saved payment methods in the third-party service for an account holder. This is useful when displaying the customer's saved payment methods in the storefront.

Learn more about implementing these methods in the [Create Payment Provider guide](https://docs.medusajs.com/references/payment/provider/index.html.md).

***

## Account Holder in Medusa Payment Flows

In the Medusa application, when a payment session is created for a registered customer, the Medusa application uses the Payment Module to create an account holder for the customer.

Consequently, the Payment Module uses the payment provider to create an account holder in the third-party service, then creates the account holder in Medusa.

This flow is only supported if the chosen payment provider has implemented the necessary [save payment methods](#save-payment-methods).


# Links between Payment Module and Other Modules

This document showcases the module links defined between the Payment Module and other Commerce Modules.

## Summary

The Payment Module has the following links to other modules:

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|Cart|PaymentCollection|Stored - one-to-one|Learn more|
|Customer|AccountHolder|Stored - many-to-many|Learn more|
|Order|PaymentCollection|Stored - one-to-many|Learn more|
|OrderClaim|PaymentCollection|Stored - one-to-many|Learn more|
|OrderExchange|PaymentCollection|Stored - one-to-many|Learn more|
|Region|PaymentProvider|Stored - many-to-many|Learn more|

***

## Cart Module

The Cart Module provides cart-related features, but not payment processing.

Medusa defines a link between the `Cart` and `PaymentCollection` data models. A cart has a payment collection which holds all the authorized payment sessions and payments made related to the cart.

Learn more about this relation in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-collection#usage-with-the-cart-module/index.html.md).

### Retrieve with Query

To retrieve the cart associated with the payment collection with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `cart.*` in `fields`:

### query.graph

```ts
const { data: paymentCollections } = await query.graph({
  entity: "payment_collection",
  fields: [
    "cart.*",
  ],
})

// paymentCollections[0].cart
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: paymentCollections } = useQueryGraphStep({
  entity: "payment_collection",
  fields: [
    "cart.*",
  ],
})

// paymentCollections[0].cart
```

### Manage with Link

To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.PAYMENT]: {
    payment_collection_id: "paycol_123",
  },
})
```

### createRemoteLinkStep

```ts
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.PAYMENT]: {
    payment_collection_id: "paycol_123",
  },
})
```

***

## Customer Module

Medusa defines a link between the `Customer` and `AccountHolder` data models, allowing payment providers to save payment methods for a customer, if the payment provider supports it.

This link is available starting from Medusa `v2.5.0`.

### Retrieve with Query

To retrieve the customer associated with an account holder with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`:

### query.graph

```ts
const { data: accountHolders } = await query.graph({
  entity: "account_holder",
  fields: [
    "customer.*",
  ],
})

// accountHolders[0].customer
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: accountHolders } = useQueryGraphStep({
  entity: "account_holder",
  fields: [
    "customer.*",
  ],
})

// accountHolders[0].customer
```

### Manage with Link

To manage the account holders of a customer, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.CUSTOMER]: {
    customer_id: "cus_123",
  },
  [Modules.PAYMENT]: {
    account_holder_id: "acchld_123",
  },
})
```

### createRemoteLinkStep

```ts
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.CUSTOMER]: {
    customer_id: "cus_123",
  },
  [Modules.PAYMENT]: {
    account_holder_id: "acchld_123",
  },
})
```

***

## Order Module

An order's payment details are stored in a payment collection. This also applies for claims and exchanges.

So, Medusa defines links between the `PaymentCollection` data model and the `Order`, `OrderClaim`, and `OrderExchange` data models.

![A diagram showcasing an example of how data models from the Order and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716554726/Medusa%20Resources/order-payment_ubdwok.jpg)

### Retrieve with Query

To retrieve the order of a payment collection with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`:

### query.graph

```ts
const { data: paymentCollections } = await query.graph({
  entity: "payment_collection",
  fields: [
    "order.*",
  ],
})

// paymentCollections[0].order
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: paymentCollections } = useQueryGraphStep({
  entity: "payment_collection",
  fields: [
    "order.*",
  ],
})

// paymentCollections[0].order
```

### Manage with Link

To manage the payment collections of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.PAYMENT]: {
    payment_collection_id: "paycol_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.PAYMENT]: {
    payment_collection_id: "paycol_123",
  },
})
```

***

## Region Module

You can specify for each region which payment providers are available. The Medusa application defines a link between the `PaymentProvider` and the `Region` data models.

![A diagram showcasing an example of how resources from the Payment and Region modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711569520/Medusa%20Resources/payment-region_jyo2dz.jpg)

This increases the flexibility of your store. For example, you only show during checkout the payment providers associated with the cart's region.

### Retrieve with Query

To retrieve the regions of a payment provider with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `regions.*` in `fields`:

### query.graph

```ts
const { data: paymentProviders } = await query.graph({
  entity: "payment_provider",
  fields: [
    "regions.*",
  ],
})

// paymentProviders[0].regions
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: paymentProviders } = useQueryGraphStep({
  entity: "payment_provider",
  fields: [
    "regions.*",
  ],
})

// paymentProviders[0].regions
```

### Manage with Link

To manage the payment providers in a region, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.REGION]: {
    region_id: "reg_123",
  },
  [Modules.PAYMENT]: {
    payment_provider_id: "pp_stripe_stripe",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.REGION]: {
    region_id: "reg_123",
  },
  [Modules.PAYMENT]: {
    payment_provider_id: "pp_stripe_stripe",
  },
})
```


# Payment Module Options

In this document, you'll learn about the options you can pass to the Payment Module.

## All Module Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`webhook\_delay\`|A number indicating the delay in milliseconds before processing a webhook event.|No|\`5000\`|
|\`webhook\_retries\`|The number of times to retry the webhook event processing in case of an error.|No|\`3\`|
|\`providers\`|An array of payment providers to install and register. Learn more |No|-|

***

## providers Option

The `providers` option is an array of payment module providers to be registered in your Medusa application.

When the Medusa application starts, these providers are registered and can be used to process payments.

For example:

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/payment",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/payment-stripe",
            id: "stripe",
            options: {
              // ...
            },
          },
        ],
      },
    },
  ],
})
```

The `providers` option is an array of objects that accept the following properties:

- `resolve`: A string indicating the package name of the module provider or the path to it.
- `id`: A string indicating the provider's unique name or ID.
- `options`: An optional object of the module provider's options.

Refer to the [Payment Module Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) documentation to learn more.


# Payment Module

In this section of the documentation, you will find resources to learn more about the Payment Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/payments/index.html.md) to learn how to manage order payments using the dashboard.

Medusa has payment related features available out-of-the-box through the Payment Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Payment Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Payment Features

- [Authorize, Capture, and Refund Payments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment/index.html.md): Authorize, capture, and refund payments for a single resource.
- [Payment Collection Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-collection/index.html.md): Store and manage all payments of a single resources, such as a cart, in payment collections.
- [Integrate Third-Party Payment Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md): Use payment providers like [Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) to handle and process payments, or integrate custom payment providers.
- [Saved Payment Methods](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/account-holder/index.html.md): Save payment methods for customers in third-party payment providers.
- [Handle Webhook Events](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/webhook-events/index.html.md): Handle webhook events from third-party providers and process the associated payment.

***

## How to Use the Payment Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-payment-collection.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createPaymentCollectionStep = createStep(
  "create-payment-collection",
  async ({}, { container }) => {
    const paymentModuleService = container.resolve(Modules.PAYMENT)

    const paymentCollection = await paymentModuleService.createPaymentCollections({
      currency_code: "usd",
      amount: 5000,
    })

    return new StepResponse({ paymentCollection }, paymentCollection.id)
  },
  async (paymentCollectionId, { container }) => {
    if (!paymentCollectionId) {
      return
    }
    const paymentModuleService = container.resolve(Modules.PAYMENT)

    await paymentModuleService.deletePaymentCollections([paymentCollectionId])
  }
)

export const createPaymentCollectionWorkflow = createWorkflow(
  "create-payment-collection",
  () => {
    const { paymentCollection } = createPaymentCollectionStep()

    return new WorkflowResponse({
      paymentCollection,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createPaymentCollectionWorkflow } from "../../workflows/create-payment-collection"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createPaymentCollectionWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createPaymentCollectionWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createPaymentCollectionWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***

## Configure Payment Module

The Payment Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options/index.html.md) for details on the module's options.

***

## Providers

Medusa provides the following payment providers out-of-the-box. You can use them to process payments for orders, returns, and other resources.

***


# Payment Steps in Checkout Flow

In this guide, you'll learn about Medusa's accept payment flow that's used in checkout.

## Overview of the Payment Flow in Checkout

The Medusa application has a built-in payment flow that allows you to accept payments from customers, typically during checkout.

This flow is designed to be flexible and extensible, allowing you to integrate with various payment providers.

The payment flow consists of the following steps:

![A diagram showcasing the payment flow's steps](https://res.cloudinary.com/dza7lstvk/image/upload/v1711566781/Medusa%20Resources/payment-flow_jblrvw.jpg)

- [Create Payment Collection](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollections): Create a payment collection associated with a cart.
  - This payment collection will hold all details related to the payment operations.
- [Show Payment Providers](https://docs.medusajs.com/api/store#payment-providers_getpaymentproviders): Show the customer the available payment providers to choose from.
  - You can integrate any [payment provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md), and you can enable them per region.
- [Create and Initialize Payment Session](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions): Create a payment session for the selected payment provider in the Medusa application, and initialize the session in the third-party payment provider.
- [Complete Cart](https://docs.medusajs.com/api/store#carts_postcartsidcomplete): Once the customer places the order, complete the cart, which involves:
  - Authorizing the payment session with the third-party payment provider.
  - If the third-party payment provider requires performing additional actions, show them to the customer, then retry cart completion.

***

## Implement Payment Checkout Step in Storefront

If you're using the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), the checkout flow is already implemented with the payment step.

If you're building a custom storefront, or you want to customize the checkout flow, you can follow the [Checkout in Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/index.html.md) guide to learn how to build the checkout flow in the storefront, including the payment step.

***

{/* TODO add section on customizng the payment flow */}

## Build a Custom Payment Flow

You can also build a custom payment flow using workflows or the Payment Module's main service.

Refer to the [Accept Payment Flow](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-flow/index.html.md) guide to learn more.


# Payment Collection

In this document, you’ll learn what a payment collection is and how the Medusa application uses it with the Cart Module.

## What's a Payment Collection?

A payment collection stores payment details related to a resource, such as a cart or an order. It’s represented by the [PaymentCollection data model](https://docs.medusajs.com/references/payment/models/PaymentCollection/index.html.md).

Every purchase or request for payment starts with a payment collection. The collection holds details necessary to complete the payment, including:

- The [payment sessions](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-session/index.html.md) that represent the payment amount to authorize.
- The [payments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment/index.html.md) that are created when a payment session is authorized. They can be captured and refunded.
- The [payment providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) that handle the processing of each payment session, including the authorization, capture, and refund.

***

## Multiple Payments

The payment collection supports multiple payment sessions and payments.

You can use this to accept payments in increments or split payments across payment providers.

![Diagram showcasing how a payment collection can have multiple payment sessions and payments](https://res.cloudinary.com/dza7lstvk/image/upload/v1711554695/Medusa%20Resources/payment-collection-multiple-payments_oi3z3n.jpg)

***

## Usage with the Cart Module

The Cart Module provides cart management features. However, it doesn’t provide any features related to accepting payment.

During checkout, the Medusa application links a cart to a payment collection, which will be used for further payment processing. It also implements the [payment flow](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-flow/index.html.md) during checkout.

![Diagram showcasing the relation between the Payment and Cart modules](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg)


# Accept Payment in Checkout Flow

In this guide, you'll learn how to implement it using workflows or the Payment Module.

## Why Implement the Payment Flow?

Medusa already provides a built-in payment flow that allows you to accept payments from customers, which you can learn about in the [Accept Payment Flow in Checkout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-checkout-flow/index.html.md) guide.

You may need to implement a custom payment flow if you have a different use case, or you're using the Payment Module separately from the Medusa application.

This guide will help you understand how to implement a payment flow using the Payment Module's main service or workflows.

You can also follow this guide to get a general understanding of how the payment flow works in the Medusa application.

***

## How to Implement the Accept Payment Flow?

For a guide on how to implement this flow in the storefront, check out [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/payment/index.html.md).

It's highly recommended to use Medusa's workflows to implement this flow. Use the Payment Module's main service for more complex cases.

### 1. Create a Payment Collection

A payment collection holds all details related to a resource’s payment operations. So, you start off by creating a payment collection.

In the Medusa application, you associate the payment collection with a cart, which is the resource that the customer is trying to pay for.

For example:

### Using Workflow

```ts
import { createPaymentCollectionForCartWorkflow } from "@medusajs/medusa/core-flows"

// ...

await createPaymentCollectionForCartWorkflow(container)
  .run({
    input: {
      cart_id: "cart_123",
    },
  })
```

### Using Service

```ts
const paymentCollection =
  await paymentModuleService.createPaymentCollections({
    currency_code: "usd",
    amount: 5000,
  })
```

### 2. Show Payment Providers

Next, you'll show the customer the available payment providers to choose from.

In the Medusa application, you need to use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve the available payment providers in a region.

### Using Query

```ts
const query = container.resolve("query")

const { data: regionPaymentProviders } = await query.graph({
  entryPoint: "region_payment_provider",
  variables: {
    filters: {
      region_id: "reg_123",
    },
  },
  fields: ["payment_providers.*"],
})

const paymentProviders = regionPaymentProviders.map(
  (relation) => relation.payment_providers
)
```

### Using Service

```ts
const paymentProviders = await paymentModuleService.listPaymentProviders()
```

### 3. Create Payment Sessions

The payment collection has one or more payment sessions, each being a payment amount to be authorized by a payment provider.

So, once the customer selects a payment provider, create a payment session for the selected payment provider.

This will also initialize the payment session in the third-party payment provider.

For example:

### Using Workflow

```ts
import { createPaymentSessionsWorkflow } from "@medusajs/medusa/core-flows"

// ...

const { result: paymentSession } = await createPaymentSessionsWorkflow(container)
  .run({
    input: {
      payment_collection_id: "paycol_123",
      provider_id: "pp_stripe_stripe",
    },
  })
```

### Using Service

```ts
const paymentSession =
  await paymentModuleService.createPaymentSession(
    paymentCollection.id,
    {
      provider_id: "pp_stripe_stripe",
      currency_code: "usd",
      amount: 5000,
      data: {
        // any necessary data for the
        // payment provider
      },
    }
  )
```

### 4. Authorize Payment Session

Once the customer places the order, you need to authorize the payment session with the third-party payment provider.

For example:

### Using Step

```ts
import { authorizePaymentSessionStep } from "@medusajs/medusa/core-flows"

// ...

authorizePaymentSessionStep({
  id: "payses_123",
  context: {},
})
```

### Using Service

```ts
const payment = authorizePaymentSessionStep({
  id: "payses_123",
  context: {},
})
```

When the payment authorization is successful, a payment is created and returned.

#### Handling Additional Action

If you used the `authorizePaymentSessionStep`, you don't need to implement this logic as it's implemented in the step.

If the payment authorization isn’t successful, whether because it requires additional action or for another reason, the method updates the payment session with the new status and throws an error.

In that case, you can catch that error and, if the session's `status` property is `requires_more`, handle the additional action, then retry the authorization.

For example:

```ts
try {
  const payment =
    await paymentModuleService.authorizePaymentSession(
      paymentSession.id,
      {}
    )
} catch (e) {
  // retrieve the payment session again
  const updatedPaymentSession = (
    await paymentModuleService.listPaymentSessions({
      id: [paymentSession.id],
    })
  )[0]

  if (updatedPaymentSession.status === "requires_more") {
    // TODO perform required action
    // TODO authorize payment again.
  }
}
```

### 5. Payment Flow Complete

The payment flow is complete once the payment session is authorized and the payment is created.

You can then:

- Complete the cart using the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) if you're using the Medusa application.
- Capture the payment either using the [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md) or [capturePayment method](https://docs.medusajs.com/references/payment/capturePayment/index.html.md).
- Refund captured amounts using the [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md) or [refundPayment method](https://docs.medusajs.com/references/payment/refundPayment/index.html.md).

Some payment providers allow capturing the payment automatically once it’s authorized. In that case, you don’t need to do it manually.


# Payment Module Provider

In this guide, you’ll learn about the Payment Module Provider and how it's used.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/regions/index.html.md) to learn how to manage the payment providers available in a region using the dashboard.

***

## What is a Payment Module Provider?

The Payment Module Provider handles payment processing in the Medusa application. It integrates third-party payment services, such as [Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md).

To authorize a payment amount with a payment provider, a payment session is created and associated with that payment provider. The payment provider is then used to handle the authorization.

After the payment session is authorized, the payment provider is associated with the resulting payment and handles its payment processing, such as to capture or refund payment.

![Diagram showcasing the communication between Medusa, the Payment Module Provider, and the third-party payment provider.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746791374/Medusa%20Resources/payment-provider-service_l4zi6m.jpg)

### List of Payment Module Providers

- [Stripe](https://docs.medusajs.com/commerce-modules/payment/payment-provider/stripe/index.html.md)

### Default Payment Provider

The Payment Module provides a `system` payment provider that acts as a placeholder payment provider.

It doesn’t handle payment processing and delegates that to the merchant. It acts similarly to a cash-on-delivery (COD) payment method.

The identifier of the system payment provider is `pp_system`.

***

## How to Create a Custom Payment Provider?

A payment provider is a module whose main service extends the `AbstractPaymentProvider` imported from `@medusajs/framework/utils`.

The module can have multiple payment provider services, where each is registered as a separate payment provider.

Refer to [this guide](https://docs.medusajs.com/references/payment/provider/index.html.md) on how to create a payment provider for the Payment Module.

After you create a payment provider, you can enable it as a payment provider in a region using the [Medusa Admin dashboard](https://docs.medusajs.com/user-guide/settings/regions/index.html.md).

***

## How are Payment Providers Registered?

### Configure Payment Module's Providers

The Payment Module accepts a `providers` option that allows you to configure the providers registered in your application.

Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options/index.html.md) guide.

### Registration on Application Start

When the Medusa application starts, it registers the Payment Module Providers defined in the `providers` option of the Payment Module.

For each Payment Module Provider, the Medusa application finds all payment provider services defined in them to register.

### PaymentProvider Data Model

A registered payment provider is represented by the [PaymentProvider data model](https://docs.medusajs.com/references/payment/models/PaymentProvider/index.html.md) in the Medusa application.

![Diagram showcasing the PaymentProvider data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1746791364/Medusa%20Resources/payment-provider-model_lx91oa.jpg)

This data model is used to reference a service in the Payment Module Provider and determine whether it's installed in the application.

The `PaymentProvider` data model has the following properties:

- `id`: The unique identifier of the Payment Module Provider. The ID's format is `pp_{identifier}_{id}`, where:
  - `identifier` is the value of the `identifier` property in the Payment Module Provider's service.
  - `id` is the value of the `id` property of the Payment Module Provider in `medusa-config.ts`.
- `is_enabled`: A boolean indicating whether the payment provider is enabled.

### How to Remove a Payment Provider?

If you remove a payment provider from the `providers` option, the Medusa application will not remove the associated `PaymentProvider` data model record.

Instead, the Medusa application will set the `is_enabled` property of the `PaymentProvider`'s record to `false`. This allows you to re-enable the payment provider later if needed by adding it back to the `providers` option.


# Stripe Module Provider

In this document, you’ll learn about the Stripe Module Provider and how to configure it in the Payment Module.

Your technical team must install the Stripe Module Provider in your Medusa application first. Then, refer to [this user guide](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details/index.html.md) to learn how to enable the Stripe payment provider in a region using the Medusa Admin dashboard.

## Register the Stripe Module Provider

### Prerequisites

- [Stripe account](https://stripe.com/)
- [Stripe Secret API Key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard)
- [For deployed Medusa applications, a Stripe webhook secret. Refer to the end of this guide for details on the URL and events.](https://docs.stripe.com/webhooks#add-a-webhook-endpoint)

The Stripe Module Provider is installed by default in your application. To use it, add it to the array of providers passed to the Payment Module in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/payment",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/payment-stripe",
            id: "stripe",
            options: {
              apiKey: process.env.STRIPE_API_KEY,
            },
          },
        ],
      },
    },
  ],
})
```

### Environment Variables

Make sure to add the necessary environment variables for the above options in `.env`:

```bash
STRIPE_API_KEY=<YOUR_STRIPE_API_KEY>
```

### Module Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`apiKey\`|A string indicating the Stripe Secret API key.|Yes|-|
|\`webhookSecret\`|A string indicating the Stripe webhook secret. This is only useful for deployed Medusa applications.|Yes|-|
|\`capture\`|Whether to automatically capture payment after authorization.|No|\`false\`|
|\`automatic\_payment\_methods\`|A boolean value indicating whether to enable Stripe's automatic payment methods. This is useful if you integrate services like Apple pay or Google pay.|No|\`false\`|
|\`payment\_description\`|A string used as the default description of a payment if none is available in cart.context.payment\_description.|No|-|
|\`oxxoExpiresDays\`|The number of days before an OXXO payment expires. Only applicable if you plan to use OXXO as a payment method.|No|\`3\`|

***

## Enable Stripe Providers in a Region

Before customers can use Stripe to complete their purchases, you must enable the Stripe payment provider(s) in the region where you want to offer this payment method.

Refer to the [user guide](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details/index.html.md) to learn how to edit a region and enable the Stripe payment provider.

***

## Stripe Payment Provider IDs

When you register the Stripe Module Provider, it registers different providers, such as basic Stripe payment, Bancontact, and more.

Each provider is registered and referenced by a unique ID made up of the format `pp_{identifier}_{id}`, where:

- `{identifier}` is the ID of the payment provider as defined in the Stripe Module Provider.
- `{id}` is the ID of the Stripe Module Provider as set in the `medusa-config.ts` file. For example, `stripe`.

Assuming you set the ID of the Stripe Module Provider to `stripe` in `medusa-config.ts`, the Medusa application will register the following payment providers:

|Provider Name|Provider ID|
|---|---|---|
|Basic Stripe Payment|\`pp\_stripe\_stripe\`|
|Bancontact Payments|\`pp\_stripe-bancontact\_stripe\`|
|BLIK Payments|\`pp\_stripe-blik\_stripe\`|
|giropay Payments|\`pp\_stripe-giropay\_stripe\`|
|iDEAL Payments|\`pp\_stripe-ideal\_stripe\`|
|Przelewy24 Payments|\`pp\_stripe-przelewy24\_stripe\`|
|PromptPay Payments|\`pp\_stripe-promptpay\_stripe\`|
|OXXO Payments (Available since |\`pp\_stripe-oxxo\_stripe\`|

***

## Setup Stripe Webhooks

For production applications, you must set up webhooks in Stripe that inform Medusa of changes and updates to payments. Refer to [Stripe's documentation](https://docs.stripe.com/webhooks#add-a-webhook-endpoint) on how to setup webhooks.

### Webhook URL

Medusa has a `{server_url}/hooks/payment/{provider_id}` API route that you can use to register webhooks in Stripe, where:

- `{server_url}` is the URL to your deployed Medusa application in server mode.
- `{provider_id}` is the ID of the provider as explained in the [Stripe Payment Provider IDs](#stripe-payment-provider-ids) section, without the `pp_` prefix.

The Stripe Module Provider supports the following payment types, and the webhook endpoint URL is different for each:

|Stripe Payment Type|Webhook Endpoint URL|
|---|---|---|
|Basic Stripe Payment|\`\{server\_url}/hooks/payment/stripe\_stripe\`|
|Bancontact Payments|\`\{server\_url}/hooks/payment/stripe-bancontact\_stripe\`|
|BLIK Payments|\`\{server\_url}/hooks/payment/stripe-blik\_stripe\`|
|giropay Payments|\`\{server\_url}/hooks/payment/stripe-giropay\_stripe\`|
|iDEAL Payments|\`\{server\_url}/hooks/payment/stripe-ideal\_stripe\`|
|Przelewy24 Payments|\`\{server\_url}/hooks/payment/stripe-przelewy24\_stripe\`|
|PromptPay Payments|\`\{server\_url}/hooks/payment/stripe-promptpay\_stripe\`|
|OXXO Payments (Available since |\`\{server\_url}/hooks/payment/stripe-oxxo\_stripe\`|

### Webhook Events

When you set up the webhook in Stripe, choose the following events to listen to:

- `payment_intent.amount_capturable_updated`
- `payment_intent.succeeded`
- `payment_intent.payment_failed`
- `payment_intent.partially_funded` (Since [v2.8.5](https://github.com/medusajs/medusa/releases/tag/v2.8.5))

***

## Useful Guides

- [Storefront guide: Add Stripe payment method during checkout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/payment/stripe/index.html.md).
- [Integrate in Next.js Starter](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter#stripe-integration/index.html.md).
- [Customize Stripe Integration in Next.js Starter](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/guides/customize-stripe/index.html.md).
- [Add Saved Payment Methods with Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/how-to-tutorials/tutorials/saved-payment-methods/index.html.md).


# Payment Session

In this document, you’ll learn what a payment session is.

## What's a Payment Session?

A payment session, represented by the [PaymentSession data model](https://docs.medusajs.com/references/payment/models/PaymentSession/index.html.md), is a payment amount to be authorized. It’s associated with a payment provider that handles authorizing it.

A payment collection can have multiple payment sessions. Using this feature, you can implement payment in installments or payments using multiple providers.

![Diagram showcasing how every payment session has a different payment provider](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565056/Medusa%20Resources/payment-session-provider_guxzqt.jpg)

***

## data Property

Payment providers may need additional data to process the payment later. For example, the ID of the session in the third-party provider.

The `PaymentSession` data model has a `data` property used to store that data. It's set by the [payment provider in Medusa](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) when the payment is initialized.

Then, when the payment session is authorized, the `data` property is used by the payment provider in Medusa to process the payment with the third-party provider.

If you're building a custom payment provider, learn more about initializing the payment session and setting the `data` property in the [Create Payment Provider](https://docs.medusajs.com/references/payment/provider/index.html.md) guide.

### data Property in the Storefront

This `data` property is accessible in the storefront as well. So, only store in it data that can be publicly shared, and data that is useful in the storefront.

For example, you can also store the client token used to initialize the payment session in the storefront with the third-party provider.

***

## Payment Session Status

The `status` property of a payment session indicates its current status. Its value can be:

- `pending`: The payment session is awaiting authorization.
- `requires_more`: The payment session requires an action before it’s authorized. For example, to enter a 3DS code.
- `authorized`: The payment session is authorized.
- `error`: An error occurred while authorizing the payment.
- `canceled`: The authorization of the payment session has been canceled.


# Payment

In this document, you’ll learn what a payment is and how it's created, captured, and refunded.

## What's a Payment?

When a payment session is authorized, a payment, represented by the [Payment data model](https://docs.medusajs.com/references/payment/models/Payment/index.html.md), is created. This payment can later be captured or refunded.

A payment carries many of the data and relations of a payment session:

- It belongs to the same payment collection.
- It’s associated with the same payment provider, which handles further payment processing.
- It stores the payment session’s `data` property in its `data` property, as it’s still useful for the payment provider’s processing.

***

## Capture Payments

When a payment is captured, a capture, represented by the [Capture data model](https://docs.medusajs.com/references/payment/models/Capture/index.html.md), is created. It holds details related to the capture, such as the amount, the capture date, and more.

The payment can also be captured incrementally, each time a capture record is created for that amount.

![A diagram showcasing how a payment's multiple captures are stored](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565445/Medusa%20Resources/payment-capture_f5fve1.jpg)

***

## Refund Payments

When a payment is refunded, a refund, represented by the [Refund data model](https://docs.medusajs.com/references/payment/models/Refund/index.html.md), is created. It holds details related to the refund, such as the amount, refund date, and more.

A payment can be refunded multiple times, and each time a refund record is created.

![A diagram showcasing how a payment's multiple refunds are stored](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565555/Medusa%20Resources/payment-refund_lgfvyy.jpg)

***

## data Property

Payment providers may need additional data to process the payment later. For example, the ID of the associated payment in the third-party provider.

The `Payment` data model has a `data` property used to store that data. The first time it's set is when the [payment provider in Medusa](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) authorizes the payment.

Then, the `data` property is passed to the Medusa payment provider when the payment is captured or refunded, allowing the payment provider to utilize the data to process the payment with the third-party provider.

If you're building a custom payment provider, learn more about authorizing and capturing the payments and setting the `data` property in the [Create Payment Provider](https://docs.medusajs.com/references/payment/provider/index.html.md) guide.


# Payment Webhook Events

In this guide, you’ll learn how you can handle payment webhook events in your Medusa application and using the Payment Module.

## What's a Payment Webhook Event?

A payment webhook event is a request sent from a third-party payment provider to your application. It indicates a change in a payment’s status.

This is useful in many cases such as:

- When a payment is processed (authorized or captured) asynchronously.
- When a payment is managed on the third-party payment provider's side.
- When a payment action on the frontend was interrupted, leading the payment to be processed without an order being created in the Medusa application.

So, it's essential to handle webhook events to ensure that your application is aware of updated payment statuses and can take appropriate actions.

***

## How to Handle Payment Webhook Events

### Webhook Listener API Route

The Medusa application has a `/hooks/payment/[identifier]_[provider]` API route out-of-the-box that allows you to listen to webhook events from third-party payment providers, where:

- `[identifier]` is the `identifier` static property defined in the payment provider. For example, `stripe`.
- `[provider]` is the ID of the provider. For example, `stripe`.

For example, when integrating basic Stripe payments with the [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md), the webhook listener route is `/hooks/payment/stripe_stripe`.

You can use this webhook listener when configuring webhook events in your third-party payment provider.

### getWebhookActionAndData Method

The webhook listener API route executes the [getWebhookActionAndData method](https://docs.medusajs.com/references/payment/getWebhookActionAndData/index.html.md) of the Payment Module's main service. This method delegates handling of incoming webhook events to the relevant payment provider.

Payment providers have a similar [getWebhookActionAndData method](https://docs.medusajs.com/references/payment/provider/index.html.md) to process the webhook event. So, if you're implementing a custom payment provider, make sure to implement it to handle webhook events.

![A diagram showcasing the steps of how the getWebhookActionAndData method words](https://res.cloudinary.com/dza7lstvk/image/upload/v1711567415/Medusa%20Resources/payment-webhook_seaocg.jpg)

If the `getWebhookActionAndData` method returns an `authorized` or `captured` action, the Medusa application will perform one of the following actions:

View the full flow of the webhook event processing in the [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md) reference.

- If the method returns an `authorized` action, Medusa will set the associated payment session to `authorized`.
- If the method returns a `captured` action, Medusa will set the associated payment session to `captured`.
- In either cases, if the cart associated with the payment session is not completed yet, Medusa will complete the cart.


# Pricing Concepts

In this guide, you’ll learn about the main concepts in the Pricing Module.

## Price Data Model

The [Price](https://docs.medusajs.com/references/pricing/models/Price/index.html.md) data model represents a specific price for a resource. For example, the price of a product variant in the USD currency.

The `Price` data model has an `amount` property that represents the monetary value of the price. It's stored as an integer in major units. For example, `$20.00` is stored as `20`, and `$20.5` is stored as `20.5`.

The `Price` data model belongs to other data models like `PriceSet` and `PriceList` to provide a variety of pricing features, as explained below.

***

## Price Set

A [PriceSet](https://docs.medusajs.com/references/pricing/models/PriceSet/index.html.md) represents a collection of prices that are linked to a resource. For example, a product variant can have a price set that includes prices in multiple currencies, such as USD and EUR.

Each of these prices is represented by the [Price data model](#price-data-model).

![A diagram showcasing the relation between the price set and price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648650/Medusa%20Resources/price-set-money-amount_xeees0.jpg)

***

## Price List

A [PriceList](https://docs.medusajs.com/references/pricing/models/PriceList/index.html.md) is a group of prices that are only enabled when their conditions and rules are satisfied. For example, you can apply special prices to customers in the VIP group.

When the conditions are met, the prices in the price list can override the default prices in a price set. Learn more in the [Price Calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) guide.

A price list has optional `start_date` and `end_date` properties that indicate the date range in which a price list can be applied.

Its associated prices are represented by the [Price data model](#price-data-model).

***

## Multi-Currency Support for Prices

The `Price` data model has a `currency_code` property that represents the currency of the price. For example, `usd` for US Dollars and `eur` for Euros.

This adds support for multi-currency pricing, allowing you to associate a resource with a price set that contains prices in multiple currencies.

For example, Medusa links a product variant from the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md) to a price set that contains prices in multiple currencies. A variant's price set would be similar to the following:

```json title="Example Price Set with Multiple Currencies"
{
  "id": "pset_123",
  "prices": [
    {
      "id": "price_123",
      "amount": 20,
      "currency_code": "usd"
    },
    {
      "id": "price_124",
      "amount": 18,
      "currency_code": "eur"
    }
  ]
}
```

When the customer views and purchases a product, Medusa selects the price based on the customer's currency.

***

## Multi-Region Support for Prices

The `Price` data model has a relation to the [PriceRule](https://docs.medusajs.com/references/pricing/models/PriceRule/index.html.md) data model that allows you to define prices that are applied based on specific conditions, such as the customer's region.

For example, Medusa allows you to specify prices for a product variant that are applied only when the customer is in a specific region. The price would be similar to the following:

```json title="Example Price with Region Rule"
{
  "id": "price_123",
  "amount": 25,
  "currency_code": "usd",
  "price_rules": [
    {
      "id": "pricerule_123",
      "attribute": "region_id",
      "value": "reg_123",
      "operator": "eq"
    }
  ]
}
```

When the customer views and purchases a product, Medusa selects the price based on the customer's region.


# Links between Pricing Module and Other Modules

This document showcases the module links defined between the Pricing Module and other Commerce Modules.

## Summary

The Pricing Module has the following links to other modules:

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|ShippingOption|PriceSet|Stored - one-to-one|Learn more|
|ProductVariant|PriceSet|Stored - one-to-one|Learn more|

***

## Fulfillment Module

The Fulfillment Module provides fulfillment-related functionalities, including shipping options that the customer chooses from when they place their order. However, it doesn't provide pricing-related functionalities for these options.

Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set.

![A diagram showcasing an example of how data models from the Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg)

### Retrieve with Query

To retrieve the shipping option of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `shipping_option.*` in `fields`:

### query.graph

```ts
const { data: priceSets } = await query.graph({
  entity: "price_set",
  fields: [
    "shipping_option.*",
  ],
})

// priceSets[0].shipping_option
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: priceSets } = useQueryGraphStep({
  entity: "price_set",
  fields: [
    "shipping_option.*",
  ],
})

// priceSets[0].shipping_option
```

### Manage with Link

To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.FULFILLMENT]: {
    shipping_option_id: "so_123",
  },
  [Modules.PRICING]: {
    price_set_id: "pset_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.FULFILLMENT]: {
    shipping_option_id: "so_123",
  },
  [Modules.PRICING]: {
    price_set_id: "pset_123",
  },
})
```

***

## Product Module

The Product Module doesn't store or manage the prices of product variants.

Medusa defines a link between the `ProductVariant` and the `PriceSet`. A product variant’s prices are stored as prices belonging to a price set.

![A diagram showcasing an example of how data models from the Pricing and Product Module are linked. The PriceSet is linked to the ProductVariant of the Product Module.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651039/Medusa%20Resources/pricing-product_m4xaut.jpg)

So, when you want to add prices for a product variant, you create a price set and add the prices to it.

You can then benefit from adding rules to prices or using the `calculatePrices` method to retrieve the price of a product variant within a specified context.

### Retrieve with Query

To retrieve the variant of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`:

### query.graph

```ts
const { data: priceSets } = await query.graph({
  entity: "price_set",
  fields: [
    "variant.*",
  ],
})

// priceSets[0].variant
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: priceSets } = useQueryGraphStep({
  entity: "price_set",
  fields: [
    "variant.*",
  ],
})

// priceSets[0].variant
```

### Manage with Link

To manage the price set of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.PRODUCT]: {
    variant_id: "variant_123",
  },
  [Modules.PRICING]: {
    price_set_id: "pset_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.PRODUCT]: {
    variant_id: "variant_123",
  },
  [Modules.PRICING]: {
    price_set_id: "pset_123",
  },
})
```


# Pricing Module

In this section of the documentation, you will find resources to learn more about the Pricing Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/price-lists/index.html.md) to learn how to manage price lists using the dashboard.

Medusa has pricing related features available out-of-the-box through the Pricing Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Pricing Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Pricing Features

- [Price Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts/index.html.md): Store and manage prices of a resource, such as a product or a variant.
- [Multi-Currency and Region Support](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts#multi-currency-support-for-prices/index.html.md): Define prices for a single resource in multiple currencies and regions.
- [Advanced Rule Engine](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules/index.html.md): Create prices with tiers and custom rules to condition prices based on different contexts.
- [Price Lists](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts#price-list/index.html.md): Group prices and apply them only in specific conditions with price lists.
- [Price Calculation Strategy](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md): Retrieve the best price in a given context and for the specified rule values.
- [Tax-Inclusive Pricing](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md): Calculate prices with taxes included in the price, and Medusa will handle calculating the taxes automatically.

***

## How to Use the Pricing Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-price-set.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createPriceSetStep = createStep(
  "create-price-set",
  async ({}, { container }) => {
    const pricingModuleService = container.resolve(Modules.PRICING)

    const priceSet = await pricingModuleService.createPriceSets({
      prices: [
        {
          amount: 500,
          currency_code: "USD",
        },
        {
          amount: 400,
          currency_code: "EUR",
          min_quantity: 0,
          max_quantity: 4,
          rules: {},
        },
      ],
    })

    return new StepResponse({ priceSet }, priceSet.id)
  },
  async (priceSetId, { container }) => {
    if (!priceSetId) {
      return
    }
    const pricingModuleService = container.resolve(Modules.PRICING)

    await pricingModuleService.deletePriceSets([priceSetId])
  }
)

export const createPriceSetWorkflow = createWorkflow(
  "create-price-set",
  () => {
    const { priceSet } = createPriceSetStep()

    return new WorkflowResponse({
      priceSet,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createPriceSetWorkflow } from "../../workflows/create-price-set"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createPriceSetWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createPriceSetWorkflow } from "../workflows/create-price-set"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createPriceSetWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createPriceSetWorkflow } from "../workflows/create-price-set"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createPriceSetWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Prices Calculation

In this guide, you'll learn how prices are calculated when you use the [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) of the Pricing Module's main service.

## calculatePrices Method

The [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) accepts the ID of one or more price sets and a context as parameters.

It returns a price object with the best-matching price for each price set.

The `calculatePrices` method is useful for retrieving the prices of a product variant or a shipping option that matches a specific context, such as a currency code, in your backend customizations.

### Calculation Context

The calculation context is an optional object passed as the second parameter to the `calculatePrices` method. It accepts rules as key-value pairs to restrict the selected prices in the price set.

For example:

```ts
const price = await pricingModuleService.calculatePrices(
  { id: [priceSetId] },
  {
    context: {
      currency_code: "eur",
      region_id: "reg_123",
    },
  }
)
```

In this example, you retrieve the prices in a price set for the specified currency code and region ID.

### Returned Price Object

For each price set, the `calculatePrices` method selects two prices:

- A calculated price: Either a price that belongs to a price list and best matches the specified context, or the same as the original price.
- An original price, which is either:
  - The same price as the calculated price if it belongs to a price list of type `override`;
  - Otherwise, a price that doesn't belong to a price list and [best matches](#original-price-selection-logic) the specified context.

Both prices are returned in an object with the following properties:

- id: (\`string\`) The ID of the price set from which the price was selected.
- is\_calculated\_price\_price\_list: (\`boolean\`) Whether the calculated price belongs to a price list.
- calculated\_amount: (\`number\`) The amount of the calculated price, or \`null\` if there isn't a calculated price. This is the amount shown to the customer.
- is\_original\_price\_price\_list: (\`boolean\`) Whether the original price belongs to a price list.
- original\_amount: (\`number\`) The amount of the original price, or \`null\` if there isn't an original price. This amount is useful for comparing with the \`calculated\_amount\`, such as to check for a discounted value.
- currency\_code: (\`string\`) The currency code of the calculated price, or \`null\` if there isn't a calculated price.
- is\_calculated\_price\_tax\_inclusive: (\`boolean\`) Whether the calculated price is tax inclusive. Learn more about tax inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx)
- is\_original\_price\_tax\_inclusive: (\`boolean\`) Whether the original price is tax inclusive. Learn more about tax inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx)
- calculated\_price: (\`object\`) The calculated price's price details.

  - id: (\`string\`) The ID of the price.

  - price\_list\_id: (\`string\`) The ID of the associated price list.

  - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`.

  - min\_quantity: (\`number\`) The price's minimum quantity condition.

  - max\_quantity: (\`number\`) The price's maximum quantity condition.
- original\_price: (\`object\`) The original price's price details.

  - id: (\`string\`) The ID of the price.

  - price\_list\_id: (\`string\`) The ID of the associated price list.

  - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`.

  - min\_quantity: (\`number\`) The price's minimum quantity condition.

  - max\_quantity: (\`number\`) The price's maximum quantity condition.

### Original Price Selection Logic

When the calculated price isn't from a price list of type `override`, the original price is selected based on the following logic:

![Diagram illustrating the original price selection logic](https://res.cloudinary.com/dza7lstvk/image/upload/v1757058523/Medusa%20Resources/original-price-calculation_sxjw3l.jpg)

1. If the context doesn't have any rules, select the default price (the price without any rules).
2. If the context has rules and there's a price that matches all the rules, select that price.
3. If the context has rules and there's no price that matches all the rules:
   - Find all the prices whose rules match at least one rule in the context.
   - Sort the matched prices by the number of matched rules in descending order.
   - Select the first price in the sorted list (the one that matches the most rules).

***

## Examples

Consider the following price set, which has a default price, prices with rules, and tiered pricing:

```ts
const priceSet = await pricingModuleService.createPriceSets({
  prices: [
    // default price
    {
      amount: 5,
      currency_code: "eur",
      rules: {},
    },
    // prices with rules
    {
      amount: 4,
      currency_code: "eur",
      rules: {
        region_id: "reg_123",
      },
    },
    {
      amount: 4.5,
      currency_code: "eur",
      rules: {
        city: "krakow",
      },
    },
    {
      amount: 3.5,
      currency_code: "eur",
      rules: {
        city: "warsaw",
        region_id: "reg_123",
      },
    },
    // tiered price
    {
      amount: 2,
      currency_code: "eur",
      min_quantity: 100,
    },
  ],
})
```

### Default Price Selection

### Code

```ts
const price = await pricingModuleService.calculatePrices(
  { id: [priceSet.id] },
  {
    context: {
      currency_code: "eur"
    }
  }
)
```

### Result

### Calculate Prices with Exact Match

### Code

```ts
const price = await pricingModuleService.calculatePrices(
  { id: [priceSet.id] },
  {
    context: {
      currency_code: "eur",
      region_id: "reg_123",
      city: "warsaw"
    }
  }
)
```

### Result

### Calculate Prices with Partial Match

### Code

```ts
const price = await pricingModuleService.calculatePrices(
  { id: [priceSet.id] },
  {
    context: {
      currency_code: "eur",
      region_id: "reg_123",
      city: "krakow"
    }
  }
)
```

### Result

### Tiered Pricing Selection

### Code

```ts
const price = await pricingModuleService.calculatePrices(
  { id: [priceSet.id] },
  {
    context: {
      cart: {
        items: [
          {
            id: "item_1",
            quantity: 150,
            // assuming the price set belongs to this variant
            variant_id: "variant_1",
            // ...
          }
        ],
        // ...
      }
    }
  }
)
```

### Result

### Price Selection with Price List

### Code

```ts
const priceList = pricingModuleService.createPriceLists([{
  title: "Summer Price List",
  description: "Price list for summer sale",
  starts_at: Date.parse("01/10/2023").toString(),
  ends_at: Date.parse("31/10/2023").toString(),
  rules: {
    region_id: ['region_123', 'region_456'],
  },
  type: "sale",
  prices: [
    {
      amount: 2,
      currency_code: "eur",
      price_set_id: priceSet.id,
    },
    {
      amount: 1.5,
      currency_code: "usd",
      price_set_id: priceSet.id,
    }
  ],
}]);

const price = await pricingModuleService.calculatePrices(
  { id: [priceSet.id] },
  {
    context: {
      currency_code: "eur",
      region_id: "reg_123",
      city: "krakow"
    }
  }
)
```

### Result


# Price Tiers and Rules

In this Pricing Module guide, you'll learn about tired prices, price rules for price sets and price lists, and how to add rules to a price.

You can manage prices and tiers using the Medusa Admin:

- [Edit shipping option prices with conditions](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations#fixed-and-conditional-shipping-option-prices/index.html.md).
- [Add price lists to set prices with conditions for product variants](https://docs.medusajs.com/user-guide/price-lists/index.html.md).

## Tiered Pricing

Each price, represented by the [Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md), has two optional properties that can be used to create tiered prices:

- `min_quantity`: The minimum quantity that must be in the cart for the price to be applied.
- `max_quantity`: The maximum quantity that can be in the cart for the price to be applied.

This is useful to set tiered pricing for resources like product variants and shipping options.

For example, you can set a variant's price to:

- `$10` by default.
- `$8` when the customer adds `10` or more of the variant to the cart.
- `$6` when the customer adds `20` or more of the variant to the cart.

These price definitions would look like this:

```json title="Example Prices"
[
  // default price
  {
    "amount": 10,
    "currency_code": "usd",
  },
  {
    "amount": 8,
    "currency_code": "usd",
    "min_quantity": 10,
    "max_quantity": 19,
  },
  {
    "amount": 6,
    "currency_code": "usd",
    "min_quantity": 20,
  },
],
```

### How to Create Tiered Prices?

When you create prices, you can specify a `min_quantity` and `max_quantity` for each price. This allows you to create tiered pricing, where the price changes based on the quantity of items in the cart.

For example:

For most use cases where you're building customizations in the Medusa application, it's highly recommended to use [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) rather than using the Pricing Module directly. Medusa's workflows already implement extensive functionalities that you can re-use in your custom flows, with reliable roll-back mechanism.

### Using Medusa Workflows

```ts highlights={tieredPricingHighlights}
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"

// ...

const { result } = await createProductsWorkflow(container)
  .run({
    input: {
      products: [{
        variants: [{
          id: "variant_1",
          prices: [
            // default price
            {
              amount: 10,
              currency_code: "usd",
            },
            {
              amount: 8,
              currency_code: "usd",
              min_quantity: 10,
              max_quantity: 19,
            },
            {
              amount: 6,
              currency_code: "usd",
              min_quantity: 20,
            },
          ],
          // ...
        }],
      }],
      // ...
    },
  })
```

### Using the Pricing Module

```ts
const priceSet = await pricingModule.addPrices({
  priceSetId: "pset_1",
  prices: [
    // default price
    {
      amount: 10,
      currency_code: "usd",
    },
    // tiered prices
    {
      amount: 8,
      currency_code: "usd",
      min_quantity: 10,
      max_quantity: 19,
    },
    {
      amount: 6,
      currency_code: "usd",
      min_quantity: 20,
    },
  ],
})
```

In this example, you create a product with a variant whose default price is `$10`. You also add two tiered prices that set the price to `$8` when the quantity is between `10` and `19`, and to `$6` when the quantity is `20` or more.

### How are Tiered Prices Applied?

The [price calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) mechanism considers the cart's items as a context when choosing the best price to apply.

For example, consider the customer added the `variant_1` product variant (created in the workflow snippet of the [above section](#how-to-create-tiered-prices)) to their cart with a quantity of `15`.

The price calculation mechanism will choose the second price, which is `$8`, because the quantity of `15` is between `10` and `19`.

If there are other rules applied to the price, they may affect the price calculation. Keep reading to learn about other price rules, and refer to the [Price Calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) guide for more details on the calculation mechanism.

***

## Price Rule

You can also restrict prices by advanced rules, such as a customer's group, zip code, or a cart's total.

Each rule of a price is represented by the [PriceRule data model](https://docs.medusajs.com/references/pricing/models/PriceRule/index.html.md).

The `Price` data model has a `rules_count` property, which indicates how many rules, represented by `PriceRule`, are applied to the price.

For example, you create a price restricted to `10557` zip codes.

![A diagram showcasing the relation between the PriceRule and Price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648772/Medusa%20Resources/price-rule-1_vy8bn9.jpg)

A price can have multiple price rules.

For example, a price can be restricted by a region and a zip code.

![A diagram showcasing the relation between the PriceRule and Price with multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709649296/Medusa%20Resources/price-rule-3_pwpocz.jpg)

### Price List Rules

Rules applied to a price list are represented by the [PriceListRule data model](https://docs.medusajs.com/references/pricing/models/PriceListRule/index.html.md).

The `rules_count` property of a `PriceList` indicates how many rules are applied to it.

![A diagram showcasing the relation between the PriceSet, PriceList, Price, RuleType, and PriceListRuleValue](https://res.cloudinary.com/dza7lstvk/image/upload/v1709641999/Medusa%20Resources/price-list_zd10yd.jpg)

### How to Create Prices with Rules?

When you create prices, you can specify rules for each price. This allows you to create complex pricing strategies based on different contexts.

For example:

For most use cases where you're building customizations in the Medusa application, it's highly recommended to use [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) rather than using the Pricing Module directly. Medusa's workflows already implement extensive functionalities that you can re-use in your custom flows, with reliable roll-back mechanism.

### Using Medusa Workflows

```ts highlights={workflowHighlights}
const { result } = await createShippingOptionsWorkflow(container)
  .run({
    input: [{
      name: "Standard Shipping",
      service_zone_id: "serzo_123",
      shipping_profile_id: "sp_123",
      provider_id: "prov_123",
      type: {
        label: "Standard",
        description: "Standard shipping",
        code: "standard",
      },
      price_type: "flat",
      prices: [
        // default price
        {
          currency_code: "usd",
          amount: 10,
          rules: [],
        },
        // price if cart total >= $100
        {
          currency_code: "usd",
          amount: 0,
          rules: [
            {
              attribute: "item_total",
              operator: "gte",
              value: 100,
            },
          ],
        },
      ],
    }],
  })
```

### Using the Pricing Module

```ts
const priceSet = await pricingModule.addPrices({
  priceSetId: "pset_1",
  prices: [
    // default price
    {
      currency_code: "usd",
      amount: 10,
      rules: {},
    },
    // price if cart total >= $100
    {
      currency_code: "usd",
      amount: 0,
      rules: {
        item_total: {
          operator: "gte",
          value: 100,
        },
      },
    },
  ],
})
```

In this example, you create a shipping option whose default price is `$10`. When the total of the cart or order using this shipping option is greater than `$100`, the shipping option's price becomes free.

### How is the Price Rule Applied?

The [price calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) mechanism considers a price applicable when the resource that this price is in matches the specified rules.

For example, a [cart object](https://docs.medusajs.com/api/store#carts_cart_schema) has an `item_total` property. So, if a shipping option has the following price:

```json
{
  "currency_code": "usd",
  "amount": 0,
  "rules": {
    "item_total": {
      "operator": "gte",
      "value": 100,
    }
  }
}
```

The shipping option's price is applied when the cart's `item_total` is greater than or equal to `$100`.

You can also apply the rule on nested relations and properties. For example, to apply a shipping option's price based on the customer's group, you can apply a rule on the `customer.group.id` attribute:

```json
{
  "currency_code": "usd",
  "amount": 0,
  "rules": {
    "customer.group.id": {
      "operator": "eq",
      "value": "cusgrp_123"
    }
  }
}
```

In this example, the price is only applied if a cart's customer belongs to the customer group of ID `cusgrp_123`.

These same rules apply to product variant prices as well, or any other resource that has a price.

***

## Examples

This section provides some examples of implementing price tiers and rules for products and shipping options.

### Product Variant Price by Quantity

### Using Medusa Workflows

```ts
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"

// ...

const { result } = await createProductsWorkflow(container)
  .run({
    input: {
      products: [{
        title: "Premium T-Shirt",
        variants: [{
          title: "Small / Black",
          prices: [
            // default price
            {
              amount: 20,
              currency_code: "usd",
            },
            // buy 5 or more, get a small discount
            {
              amount: 18,
              currency_code: "usd",
              min_quantity: 5,
              max_quantity: 9,
            },
            // buy 10 or more, get a bigger discount
            {
              amount: 15,
              currency_code: "usd",
              min_quantity: 10,
            },
          ],
        }],
      }],
    },
  })
```

### Using the Pricing Module

```ts
const priceSet = await pricingModule.addPrices({
  // this is the price set of the product variant
  priceSetId: "pset_product_123",
  prices: [
    // default price: $20.00
    {
      amount: 20,
      currency_code: "usd",
    },
    // buy 5-9 shirts: $18.00 each
    {
      amount: 18,
      currency_code: "usd",
      min_quantity: 5,
      max_quantity: 9,
    },
    // buy 10+ shirts: $15.00 each
    {
      amount: 15,
      currency_code: "usd",
      min_quantity: 10,
    },
  ],
})
```

### Product Variant Price by Customer Group

### Using Medusa Workflows

```ts
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"

// ...

const { result } = await createProductsWorkflow(container)
  .run({
    input: {
      products: [{
        title: "Premium T-Shirt",
        variants: [{
          title: "Small / Black",
          prices: [
            // default price
            {
              amount: 40,
              currency_code: "usd",
            },
            // discounted price for VIP customers
            {
              amount: 30,
              currency_code: "usd",
              rules: {
                "customer.groups.id": "cusgrp_vip123",
              },
            },
            // special price for wholesale customers
            {
              amount: 20,
              currency_code: "usd",
              rules: {
                "customer.groups.id": "cusgrp_wholesale456",
              },
            },
          ],
        }],
      }],
    },
  })
```

### Using the Pricing Module

```ts
const priceSet = await pricingModule.addPrices({
  // this is the price set of the product variant
  priceSetId: "pset_product_123",
  prices: [
    // default price
    {
      amount: 40,
      currency_code: "usd",
    },
    // discounted price for VIP customers
    {
      amount: 30,
      currency_code: "usd",
      rules: {
        "customer.groups.id": "cusgrp_vip123",
      },
    },
    // special price for wholesale customers
    {
      amount: 20,
      currency_code: "usd",
      rules: {
        "customer.groups.id": "cusgrp_wholesale456",
      },
    },
  ],
})
```

### Free Shipping for Orders Above a Certain Amount

### Using Medusa Workflows

```ts
const { result } = await createShippingOptionsWorkflow(container)
  .run({
    input: [{
      name: "Standard Shipping",
      service_zone_id: "serzo_123",
      shipping_profile_id: "sp_123",
      provider_id: "prov_123",
      type: {
        label: "Standard",
        description: "Standard shipping (2-5 business days)",
        code: "standard",
      },
      price_type: "flat",
      prices: [
        // regular shipping price
        {
          currency_code: "usd",
          amount: 10,
          rules: [],
        },
        // free shipping for carts over $100
        {
          currency_code: "usd",
          amount: 0,
          rules: [
            {
              attribute: "item_total",
              operator: "gte",
              value: 100,
            },
          ],
        },
      ],
    }],
  })
```

### Using the Pricing Module

```ts
const priceSet = await pricingModule.addPrices({
  priceSetId: "pset_shipping_123",
  prices: [
    // regular shipping price
    {
      currency_code: "usd",
      amount: 10,
      rules: {},
    },
    // free shipping for orders over $100
    {
      currency_code: "usd",
      amount: 0,
      rules: {
        item_total: [
          {
            operator: "gte",
            value: 100,
          },
        ],
      },
    },
  ],
})
```

### Shipping Prices Based on Customer Groups

### Using Medusa Workflows

```ts
const { result } = await createShippingOptionsWorkflow(container)
  .run({
    input: [{
      name: "Express Shipping",
      service_zone_id: "serzo_456",
      shipping_profile_id: "sp_456",
      provider_id: "prov_456",
      type: {
        label: "Express",
        description: "Express shipping (1-2 business days)",
        code: "express",
      },
      price_type: "flat",
      prices: [
        // regular express shipping price
        {
          currency_code: "usd",
          amount: 20,
          rules: [],
        },
        // discounted express shipping for VIP customers
        {
          currency_code: "usd",
          amount: 15,
          rules: [
            {
              attribute: "customer.groups.id",
              operator: "in",
              value: ["cusgrp_vip123"],
            },
          ],
        },
        // special rate for B2B customers
        {
          currency_code: "usd",
          amount: 13,
          rules: [
            {
              attribute: "customer.groups.id",
              operator: "in",
              value: ["cusgrp_b2b789"],
            },
          ],
        },
      ],
    }],
  })
```

### Using the Pricing Module

```ts
const priceSet = await pricingModule.addPrices({
  priceSetId: "pset_shipping_456",
  prices: [
    // regular express shipping
    {
      currency_code: "usd",
      amount: 20,
      rules: {},
    },
    // VIP customer express shipping
    {
      currency_code: "usd",
      amount: 15,
      rules: {
        "customer.groups.id": [
          {
            operator: "in",
            value: ["cusgrp_vip123"],
          },
        ],
      },
    },
    // B2B customer express shipping
    {
      currency_code: "usd",
      amount: 13,
      rules: {
        "customer.groups.id": [
          {
            operator: "in",
            value: ["cusgrp_b2b789"],
          },
        ],
      },
    },
  ],
})
```

### Combined Tiered and Rule-Based Shipping Pricing

### Using Medusa Workflows

```ts
const { result } = await createShippingOptionsWorkflow(container)
  .run({
    input: [{
      name: "Bulk Shipping",
      service_zone_id: "serzo_789",
      shipping_profile_id: "sp_789",
      provider_id: "prov_789",
      type: {
        label: "Bulk",
        description: "Shipping for bulk orders",
        code: "bulk",
      },
      price_type: "flat",
      prices: [
        // base shipping price
        {
          currency_code: "usd",
          amount: 20,
          rules: [],
        },
        // discounted shipping for 5-10 items
        {
          currency_code: "usd",
          amount: 18,
          min_quantity: 5,
          max_quantity: 10,
          rules: [],
        },
        // heavily discounted shipping for 10+ items
        {
          currency_code: "usd",
          amount: 15,
          min_quantity: 11,
          rules: [],
        },
        // free shipping for VIP customers with carts over $200
        {
          currency_code: "usd",
          amount: 0,
          rules: {
            "customer.groups.id": [
              {
                operator: "in",
                value: ["cusgrp_vip123"],
              },
            ],
            item_total: [
              {
                operator: "gte",
                value: 200,
              },
            ],
          },
        },
      ],
    }],
  })
```

### Using the Pricing Module

```ts
const priceSet = await pricingModule.addPrices({
  priceSetId: "pset_shipping_789",
  prices: [
    // base shipping price: $20.00
    {
      currency_code: "usd",
      amount: 20,
      rules: {},
    },
    // 5-10 items: $18.00
    {
      currency_code: "usd",
      amount: 18,
      min_quantity: 5,
      max_quantity: 10,
      rules: {},
    },
    // 11+ items: $15.00
    {
      currency_code: "usd",
      amount: 15,
      min_quantity: 11,
      rules: {},
    },
    // VIP customers with orders over $200: FREE
    {
      currency_code: "usd",
      amount: 0,
      rules: [
        {
          attribute: "customer.groups.id",
          operator: "in",
          value: ["cusgrp_vip123"],
        },
        {
          attribute: "item_total",
          operator: "gte",
          value: 200,
        },
      ],
    },
  ],
})
```

### Shipping Prices Based on Geographical Rules

### Using Medusa Workflows

```ts
const { result } = await createShippingOptionsWorkflow(container)
  .run({
    input: [{
      name: "Regional Shipping",
      service_zone_id: "serzo_geo123",
      shipping_profile_id: "sp_standard",
      provider_id: "prov_standard",
      type: {
        label: "Standard",
        description: "Standard shipping with regional pricing",
        code: "standard_regional",
      },
      price_type: "flat",
      prices: [
        // default shipping price
        {
          currency_code: "usd",
          amount: 15,
          rules: [],
        },
        // special price for specific zip codes (metropolitan areas)
        {
          currency_code: "usd",
          amount: 10,
          rules: [
            {
              attribute: "shipping_address.postal_code",
              operator: "in",
              value: ["10001", "10002", "10003", "90001", "90002", "90003"],
            },
          ],
        },
        // higher price for remote areas
        {
          currency_code: "usd",
          amount: 25,
          rules: [
            {
              attribute: "shipping_address.postal_code",
              operator: "in",
              value: ["99501", "99502", "99503", "00601", "00602", "00603"],
            },
          ],
        },
        // different price for a specific region
        {
          currency_code: "usd",
          amount: 12,
          rules: [
            {
              attribute: "region.id",
              operator: "eq",
              value: "reg_123",
            },
          ],
        },
        // different price for a specific country
        {
          currency_code: "usd",
          amount: 20,
          rules: [
            {
              attribute: "shipping_address.country_code",
              operator: "eq",
              value: "ca",
            },
          ],
        },
      ],
    }],
  })
```

### Using the Pricing Module

```ts
const priceSet = await pricingModule.addPrices({
  priceSetId: "pset_shipping_geo",
  prices: [
    // default shipping price: $15.00
    {
      currency_code: "usd",
      amount: 15,
      rules: {},
    },
    // metropolitan areas: $10.00
    {
      currency_code: "usd",
      amount: 10,
      rules: {
        "shipping_address.postal_code": [
          {
            operator: "in",
            value: ["10001", "10002", "10003", "90001", "90002", "90003"],
          },
        ],
      },
    },
    // remote areas: $25.00
    {
      currency_code: "usd",
      amount: 25,
      rules: {
        "shipping_address.postal_code": [
          {
            operator: "in",
            value: ["99501", "99502", "99503", "00601", "00602", "00603"],
          },
        ],
      },
    },
    // Northeast region: $12.00
    {
      currency_code: "usd",
      amount: 12,
      rules: {
        "region.id": "reg_123",
      },
    },
    // Canada: $20.00
    {
      currency_code: "usd",
      amount: 20,
      rules: {
        "shipping_address.country_code": "ca",
      },
    },
  ],
})
```


# Tax-Inclusive Pricing

In this document, you’ll learn about tax-inclusive pricing and how it's used when calculating prices.

## What is Tax-Inclusive Pricing?

A tax-inclusive price is a price of a resource that includes taxes. Medusa calculates the tax amount from the price rather than adds the amount to it.

For example, if a product’s price is $50, the tax rate is 2%, and tax-inclusive pricing is enabled, then the product's price is $49, and the applied tax amount is $1.

***

## How is Tax-Inclusive Pricing Set?

The [PricePreference data model](https://docs.medusajs.com/references/pricing/models/PricePreference/index.html.md) holds the tax-inclusive setting for a context. It has two properties that indicate the context:

- `attribute`: The name of the attribute to compare against. For example, `region_id` or `currency_code`.
- `value`: The attribute’s value. For example, `reg_123` or `usd`.

Only `region_id` and `currency_code` are supported as an `attribute` at the moment.

The `is_tax_inclusive` property indicates whether tax-inclusivity is enabled in the specified context.

For example:

```json
{
  "attribute": "currency_code",
  "value": "USD",
  "is_tax_inclusive": true,
}
```

In this example, tax-inclusivity is enabled for the `USD` currency code.

***

## Tax-Inclusive Pricing in Price Calculation

### Tax Context

As mentioned in the [Price Calculation documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), The `calculatePrices` method accepts as a parameter a calculation context.

To get accurate tax results, pass the `region_id` and / or `currency_code` in the calculation context.

### Returned Tax Properties

The `calculatePrices` method returns two properties related to tax-inclusivity:

Learn more about the returned properties in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md).

- `is_calculated_price_tax_inclusive`: Whether the selected `calculated_price` is tax-inclusive.
- `is_original_price_tax_inclusive` : Whether the selected `original_price` is tax-inclusive.

A price is considered tax-inclusive if:

1. It belongs to the region or currency code specified in the calculation context;
2. and the region or currency code has a price preference with `is_tax_inclusive` enabled.

### Tax Context Precedence

A region’s price preference’s `is_tax_inclusive`'s value takes higher precedence in determining whether a price is tax-inclusive if:

- both the `region_id` and `currency_code` are provided in the calculation context;
- the selected price belongs to the region;
- and the region has a price preference

***

## Tax-Inclusive Pricing with Promotions

When you enable tax-inclusive prices for regions or currencies, this can impact how promotions are applied to the cart. So, it's recommended to enable tax-inclusiveness for promotions as well.

Learn more in the [Tax-Inclusive Promotions](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/promotion-taxes/index.html.md) guide.


# Calculate Product Variant Price with Taxes

In this document, you'll learn how to calculate a product variant's price with taxes.

## Step 0: Resolve Resources

You'll need the following resources for the taxes calculation:

1. [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve the product's variants' prices for a context. Learn more about that in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price/index.html.md).
2. The Tax Module's main service to get the tax lines for each product.

```ts
// other imports...
import {
  Modules,
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

// In an API route, workflow step, etc...
const query = container.resolve(ContainerRegistrationKeys.QUERY)
const taxModuleService = container.resolve(
  Modules.TAX
)
```

***

## Step 1: Retrieve Prices for a Context

After resolving the resources, use Query to retrieve the products with the variants' prices for a context:

Learn more about retrieving product variants' prices for a context in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price/index.html.md).

```ts
import { QueryContext } from "@medusajs/framework/utils"

// ...

const { data: products } = await query.graph({
  entity: "product",
  fields: [
    "*",
    "variants.*",
    "variants.calculated_price.*",
  ],
  filters: {
    id: "prod_123",
  },
  context: {
    variants: {
      calculated_price: QueryContext({
        region_id: "region_123",
        currency_code: "usd",
      }),
    },
  },
})
```

***

## Step 2: Get Tax Lines for Products

To retrieve the tax line of each product, first, add the following utility method:

```ts
// other imports...
import {
  HttpTypes,
  TaxableItemDTO,
} from "@medusajs/framework/types"

// ...
const asTaxItem = (product: HttpTypes.StoreProduct): TaxableItemDTO[] => {
  return product.variants
    ?.map((variant) => {
      if (!variant.calculated_price) {
        return
      }

      return {
        id: variant.id,
        product_id: product.id,
        product_name: product.title,
        product_categories: product.categories?.map((c) => c.name),
        product_category_id: product.categories?.[0]?.id,
        product_sku: variant.sku,
        product_type: product.type,
        product_type_id: product.type_id,
        quantity: 1,
        unit_price: variant.calculated_price.calculated_amount,
        currency_code: variant.calculated_price.currency_code,
      }
    })
    .filter((v) => !!v) as unknown as TaxableItemDTO[]
}
```

This formats the products as items to calculate tax lines for.

Then, use it when retrieving the tax lines of the products retrieved earlier:

```ts
// other imports...
import {
  ItemTaxLineDTO,
} from "@medusajs/framework/types"

// ...
const taxLines = (await taxModuleService.getTaxLines(
  products.map(asTaxItem).flat(),
  {
    // example of context properties. You can pass other ones.
    address: {
      country_code,
    },
  }
)) as unknown as ItemTaxLineDTO[]
```

You use the Tax Module's main service's [getTaxLines method](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md) to retrieve the tax line.

For the first parameter, you use the `asTaxItem` function to format the products as expected by the `getTaxLines` method.

For the second parameter, you pass the current context. You can pass other details such as the customer's ID.

Learn about the other context properties to pass in [the getTaxLines method's reference](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md).

***

## Step 3: Calculate Price with Tax for Variant

To calculate the price with and without taxes for a variant, first, group the tax lines retrieved in the previous step by variant IDs:

```ts highlights={taxLineHighlights}
const taxLinesMap = new Map<string, ItemTaxLineDTO[]>()
taxLines.forEach((taxLine) => {
  const variantId = taxLine.line_item_id
  if (!taxLinesMap.has(variantId)) {
    taxLinesMap.set(variantId, [])
  }

  taxLinesMap.get(variantId)?.push(taxLine)
})
```

Notice that the variant's ID is stored in the `line_item_id` property of a tax line since tax lines are used for line items in a cart.

Then, loop over the products and their variants to retrieve the prices with and without taxes:

```ts highlights={calculateTaxHighlights}
// other imports...
import {
  calculateAmountsWithTax,
} from "@medusajs/framework/utils"

// ...
products.forEach((product) => {
  product.variants?.forEach((variant) => {
    if (!variant.calculated_price) {
      return
    }

    const taxLinesForVariant = taxLinesMap.get(variant.id) || []
    const { priceWithTax, priceWithoutTax } = calculateAmountsWithTax({
      taxLines: taxLinesForVariant,
      amount: variant.calculated_price!.calculated_amount!,
      includesTax:
        variant.calculated_price!.is_calculated_price_tax_inclusive!,
    })

    // do something with prices...
  })
})
```

For each product variant, you:

1. Retrieve its tax lines from the `taxLinesMap`.
2. Calculate its prices with and without taxes using the `calculateAmountsWithTax` from the Medusa Framework.
3. The `calculateAmountsWithTax` function returns an object having two properties:
   - `priceWithTax`: The variant's price with the taxes applied.
   - `priceWithoutTax`: The variant's price without taxes applied.


# Get Product Variant Prices using Query

In this document, you'll learn how to retrieve product variant prices in the Medusa application using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).

The Product Module doesn't provide pricing functionalities. The Medusa application links the Product Module's `ProductVariant` data model to the Pricing Module's `PriceSet` data model.

So, to retrieve data across the linked records of the two modules, you use Query.

## Retrieve All Product Variant Prices

To retrieve all product variant prices, retrieve the product using Query and include among its fields `variants.prices.*`.

For example:

```ts highlights={[["6"]]}
const { data: products } = await query.graph({
  entity: "product",
  fields: [
    "*",
    "variants.*",
    "variants.prices.*",
  ],
  filters: {
    id: [
      "prod_123",
    ],
  },
})
```

Each variant in the retrieved products has a `prices` array property with all the product variant prices. Each price object has the properties of the [Pricing Module's Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md).

***

## Retrieve Calculated Price for a Context

The Pricing Module can calculate prices of a variant based on a [context](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), such as the region ID or the currency code.

Learn more about prices calculation in [this Pricing Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md).

To retrieve calculated prices of variants based on a context, retrieve the products using Query and:

- Pass `variants.calculated_price.*` in the `fields` property.
- Pass a `context` property in the object parameter. Its value is an object of objects that sets the context for the retrieved fields.

For example:

```ts highlights={[["10"], ["15"], ["16"], ["17"], ["18"], ["19"], ["20"], ["21"], ["22"]]}
import { QueryContext } from "@medusajs/framework/utils"

// ...

const { data: products } = await query.graph({
  entity: "product",
  fields: [
    "*",
    "variants.*",
    "variants.calculated_price.*",
  ],
  filters: {
    id: "prod_123",
  },
  context: {
    variants: {
      calculated_price: QueryContext({
        region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS",
        currency_code: "eur",
      }),
    },
  },
})
```

For the context of the product variant's calculated price, you pass an object to `context` with the property `variants`, whose value is another object with the property `calculated_price`.

`calculated_price`'s value is created using `QueryContext` from the Modules SDK, passing it a [calculation context object](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md).

Each variant in the retrieved products has a `calculated_price` object. Learn more about its properties in [this Pricing Module guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md).


# Get Product Variant Inventory Quantity

In this guide, you'll learn how to retrieve the available inventory quantity of a product variant in your Medusa application customizations. That includes API routes, workflows, subscribers, scheduled jobs, and any resource that can access the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

Refer to the [Retrieve Product Variant Inventory](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/products/inventory/index.html.md) storefront guide.

## Understanding Product Variant Inventory Availability

Product variants have a `manage_inventory` boolean field that indicates whether the Medusa application manages the inventory of the product variant.

When `manage_inventory` is disabled, the Medusa application always considers the product variant to be in stock. So, you can't retrieve the inventory quantity for those products.

When `manage_inventory` is enabled, the Medusa application tracks the inventory of the product variant using the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md). For example, when a customer purchases a product variant, the Medusa application decrements the stocked quantity of the product variant.

This guide explains how to retrieve the inventory quantity of a product variant when `manage_inventory` is enabled.

***

## Retrieve Product Variant Inventory

To retrieve the inventory quantity of a product variant, use the `getVariantAvailability` utility function imported from `@medusajs/framework/utils`. It returns the available quantity of the product variant.

For example:

```ts highlights={variantAvailabilityHighlights}
import { getVariantAvailability } from "@medusajs/framework/utils"

// ...

// use req.scope instead of container in API routes
const query = container.resolve("query")

const availability = await getVariantAvailability(query, {
  variant_ids: ["variant_123"],
  sales_channel_id: "sc_123",
})
```

A product variant's inventory quantity is set per [stock location](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md). This stock location is linked to a [sales channel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md).

So, to retrieve the inventory quantity of a product variant using `getVariantAvailability`, you need to also provide the ID of the sales channel to retrieve the inventory quantity in.

Refer to the [Retrieve Sales Channel to Use](#retrieve-sales-channel-to-use) section to learn how to retrieve the sales channel ID to use in the `getVariantAvailability` function.

### Parameters

The `getVariantAvailability` function accepts the following parameters:

- query: (Query) Instance of Query to retrieve the necessary data.
- options: (\`object\`) The options to retrieve the variant availability.

  - variant\_ids: (\`string\[]\`) The IDs of the product variants to retrieve their inventory availability.

  - sales\_channel\_id: (\`string\`) The ID of the sales channel to retrieve the variant availability in.

### Returns

The `getVariantAvailability` function resolves to an object whose keys are the IDs of each product variant passed in the `variant_ids` parameter.

The value of each key is an object with the following properties:

- availability: (\`number\`) The available quantity of the product variant in the stock location linked to the sales channel. If \`manage\_inventory\` is disabled, this value is \`0\`.
- sales\_channel\_id: (\`string\`) The ID of the sales channel that the availability is scoped to.

For example, the object may look like this:

```json title="Example result"
{
  "variant_123": {
    "availability": 10,
    "sales_channel_id": "sc_123"
  }
}
```

***

## Retrieve Sales Channel to Use

To retrieve the sales channel ID to use in the `getVariantAvailability` function, you can either:

- Use the sales channel of the request's scope.
- Use the sales channel that the variant's product is available in.

### Method 1: Use Sales Channel Scope in Store Routes

Requests sent to API routes starting with `/store` must include a [publishable API key in the request header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md). This scopes the request to one or more sales channels associated with the publishable API key.

So, if you're retrieving the variant inventory availability in an API route starting with `/store`, you can access the sales channel using the `publishable_key_context.sales_channel_ids` property of the request object:

```ts highlights={salesChannelScopeHighlights}
import { MedusaStoreRequest, MedusaResponse } from "@medusajs/framework/http"
import { getVariantAvailability } from "@medusajs/framework/utils"

export async function GET(
  req: MedusaStoreRequest,
  res: MedusaResponse
) {
  const query = req.scope.resolve("query")
  const sales_channel_ids = req.publishable_key_context.sales_channel_ids

  const availability = await getVariantAvailability(query, {
    variant_ids: ["variant_123"],
    sales_channel_id: sales_channel_ids[0],
  })

  res.json({
    availability,
  })
}
```

In this example, you retrieve the scope's sales channel IDs using `req.publishable_key_context.sales_channel_ids`, whose value is an array of IDs.

Then, you pass the first sales channel ID to the `getVariantAvailability` function to retrieve the inventory availability of the product variant in that sales channel.

Notice that the request object's type is `MedusaStoreRequest` instead of `MedusaRequest` to ensure the availability of the `publishable_key_context` property.

### Method 2: Use Product's Sales Channel

A product is linked to the sales channels it's available in. So, you can retrieve the details of the variant's product, including its sales channels.

For example:

```ts highlights={productSalesChannelHighlights}
import { getVariantAvailability } from "@medusajs/framework/utils"

// ...

// use req.scope instead of container in API routes
const query = container.resolve("query")

const { data: variants } = await query.graph({
  entity: "variant",
  fields: ["id", "product.sales_channels.*"],
  filters: {
    id: "variant_123",
  },
})

const availability = await getVariantAvailability(query, {
  variant_ids: ["variant_123"],
  sales_channel_id: variants[0].product!.sales_channels![0]!.id,
})
```

In this example, you retrieve the sales channels of the variant's product using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).

You pass the ID of the variant as a filter, and you specify `product.sales_channels.*` as the fields to retrieve. This retrieves the sales channels linked to the variant's product.

Then, you pass the first sales channel ID to the `getVariantAvailability` function to retrieve the inventory availability of the product variant in that sales channel.


# Links between Product Module and Other Modules

This document showcases the module links defined between the Product Module and other Commerce Modules.

## Summary

The Product Module has the following links to other modules:

Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|LineItem|Product|Read-only - has one|Learn more|
|Product|ShippingProfile|Stored - many-to-one|Learn more|
|ProductVariant|InventoryItem|Stored - many-to-many|Learn more|
|OrderLineItem|Product|Read-only - has one|Learn more|
|ProductVariant|PriceSet|Stored - one-to-one|Learn more|
|Product|SalesChannel|Stored - many-to-many|Learn more|
|Product|Translation|Read-only - one-to-many|Learn more|
|ProductVariant|Translation|Read-only - one-to-many|Learn more|
|ProductCategory|Translation|Read-only - one-to-many|Learn more|
|ProductCollection|Translation|Read-only - one-to-many|Learn more|
|ProductTag|Translation|Read-only - one-to-many|Learn more|
|ProductType|Translation|Read-only - one-to-many|Learn more|
|ProductOption|Translation|Read-only - one-to-many|Learn more|
|ProductOptionValue|Translation|Read-only - one-to-many|Learn more|

***

## Cart Module

Medusa defines read-only links between:

- The [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItem` data model and the `Product` data model. Because the link is read-only from the `LineItem`'s side, you can only retrieve the product of a line item, and not the other way around.
- The `ProductVariant` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItem` data model. Because the link is read-only from the `LineItem`'s side, you can only retrieve the variant of a line item, and not the other way around.

### Retrieve with Query

To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`:

To retrieve the product, pass `product.*` in `fields`.

### query.graph

```ts
const { data: lineItems } = await query.graph({
  entity: "line_item",
  fields: [
    "variant.*",
  ],
})

// lineItems[0].variant
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: lineItems } = useQueryGraphStep({
  entity: "line_item",
  fields: [
    "variant.*",
  ],
})

// lineItems[0].variant
```

***

## Fulfillment Module

Medusa defines a link between the `Product` data model and the `ShippingProfile` data model of the Fulfillment Module. Each product must belong to a shipping profile.

This link is introduced in [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0).

### Retrieve with Query

To retrieve the shipping profile of a product with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `shipping_profile.*` in `fields`:

### query.graph

```ts
const { data: products } = await query.graph({
  entity: "product",
  fields: [
    "shipping_profile.*",
  ],
})

// products[0].shipping_profile
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: products } = useQueryGraphStep({
  entity: "product",
  fields: [
    "shipping_profile.*",
  ],
})

// products[0].shipping_profile
```

### Manage with Link

To manage the shipping profile of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  [Modules.FULFILLMENT]: {
    shipping_profile_id: "sp_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  [Modules.FULFILLMENT]: {
    shipping_profile_id: "sp_123",
  },
})
```

***

## Inventory Module

The Inventory Module provides inventory-management features for any stock-kept item.

Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. Each product variant has different inventory details.

![A diagram showcasing an example of how data models from the Product and Inventory modules are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709652779/Medusa%20Resources/product-inventory_kmjnud.jpg)

When the `manage_inventory` property of a product variant is enabled, you can manage the variant's inventory in different locations through this relation.

Learn more about product variant's inventory management in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md).

### Retrieve with Query

To retrieve the inventory items of a product variant with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `inventory_items.*` in `fields`:

### query.graph

```ts
const { data: variants } = await query.graph({
  entity: "variant",
  fields: [
    "inventory_items.*",
  ],
})

// variants[0].inventory_items
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: variants } = useQueryGraphStep({
  entity: "variant",
  fields: [
    "inventory_items.*",
  ],
})

// variants[0].inventory_items
```

### Manage with Link

To manage the inventory items of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.PRODUCT]: {
    variant_id: "variant_123",
  },
  [Modules.INVENTORY]: {
    inventory_item_id: "iitem_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.PRODUCT]: {
    variant_id: "variant_123",
  },
  [Modules.INVENTORY]: {
    inventory_item_id: "iitem_123",
  },
})
```

***

## Order Module

Medusa defines read-only links between:

- the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `OrderLineItem` data model and the `Product` data model. Because the link is read-only from the `OrderLineItem`'s side, you can only retrieve the product of an order line item, and not the other way around.
- the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `OrderLineItem` data model and the `ProductVariant` data model. Because the link is read-only from the `OrderLineItem`'s side, you can only retrieve the variant of an order line item, and not the other way around.

### Retrieve with Query

To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`:

To retrieve the product, pass `product.*` in `fields`.

### query.graph

```ts
const { data: lineItems } = await query.graph({
  entity: "order_line_item",
  fields: [
    "variant.*",
  ],
})

// lineItems[0].variant
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: lineItems } = useQueryGraphStep({
  entity: "order_line_item",
  fields: [
    "variant.*",
  ],
})

// lineItems[0].variant
```

***

## Pricing Module

The Product Module doesn't provide pricing-related features.

Instead, Medusa defines a link between the `ProductVariant` and the `PriceSet` data models. A product variant’s prices are stored belonging to a price set.

![A diagram showcasing an example of how data models from the Pricing and Product Module are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651464/Medusa%20Resources/product-pricing_vlxsiq.jpg)

So, to add prices for a product variant, create a price set and add the prices to it.

### Retrieve with Query

To retrieve the price set of a variant with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`:

### query.graph

```ts
const { data: variants } = await query.graph({
  entity: "variant",
  fields: [
    "price_set.*",
  ],
})

// variants[0].price_set
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: variants } = useQueryGraphStep({
  entity: "variant",
  fields: [
    "price_set.*",
  ],
})

// variants[0].price_set
```

### Manage with Link

To manage the price set of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.PRODUCT]: {
    variant_id: "variant_123",
  },
  [Modules.PRICING]: {
    price_set_id: "pset_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.PRODUCT]: {
    variant_id: "variant_123",
  },
  [Modules.PRICING]: {
    price_set_id: "pset_123",
  },
})
```

***

## Sales Channel Module

The Sales Channel Module provides functionalities to manage multiple selling channels in your store.

Medusa defines a link between the `Product` and `SalesChannel` data models. A product can have different availability in different sales channels.

![A diagram showcasing an example of how data models from the Product and Sales Channel modules are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651840/Medusa%20Resources/product-sales-channel_t848ik.jpg)

### Retrieve with Query

To retrieve the sales channels of a product with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`:

### query.graph

```ts
const { data: products } = await query.graph({
  entity: "product",
  fields: [
    "sales_channels.*",
  ],
})

// products[0].sales_channels
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: products } = useQueryGraphStep({
  entity: "product",
  fields: [
    "sales_channels.*",
  ],
})

// products[0].sales_channels
```

### Manage with Link

To manage the sales channels of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
})
```

***

## Translation Module

### Prerequisites



Medusa defines read-only links from several data models in the Product Module to the [Translation](https://docs.medusajs.com/references/translation/models/Translation/index.html.md) data model in the [Translation Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/translation/index.html.md):

- `Product` (one-to-many) to `Translation`, and vice versa.
- `ProductVariant` (one-to-many) to `Translation`. You can only retrieve the translations of a product variant, and not the other way around.
- `ProductCategory` (one-to-many) to `Translation`. You can only retrieve the translations of a product category, and not the other way around.
- `ProductCollection` (one-to-many) to `Translation`. You can only retrieve the translations of a product collection, and not the other way around.
- `ProductTag` (one-to-many) to `Translation`. You can only retrieve the translations of a product tag, and not the other way around.
- `ProductType` (one-to-many) to `Translation`. You can only retrieve the translations of a product type, and not the other way around.
- `ProductOption` (one-to-many) to `Translation`. You can only retrieve the translations of a product option, and not the other way around.
- `ProductOptionValue` (one-to-many) to `Translation`. You can only retrieve the translations of a product option value, and not the other way around.

### Retrieve with Query

To retrieve the translations of a product with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `translations.*` in `fields`:

You can pass the `translations.*` field when querying any of the above-mentioned data models to retrieve their associated translations.

### query.graph

```ts
const { data: products } = await query.graph({
  entity: "product",
  fields: [
    "translations.*",
  ],
})

// products[0].translations
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: products } = useQueryGraphStep({
  entity: "product",
  fields: [
    "translations.*",
  ],
})

// products[0].translations
```


# Product Module

In this section of the documentation, you will find resources to learn more about the Product Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/products/index.html.md) to learn how to manage products using the dashboard.

Medusa has product related features available out-of-the-box through the Product Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Product Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Product Features

- [Products Management](https://docs.medusajs.com/references/product/models/Product/index.html.md): Store and manage products. Products have custom options, such as color or size, and each variant in the product sets the value for these options.
- [Product Organization](https://docs.medusajs.com/references/product/models/index.html.md): The Product Module provides different data models used to organize products, including categories, collections, tags, and more.
- [Bundled and Multi-Part Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products.
- [Tiered Pricing and Price Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules/index.html.md): Set prices for product variants with tiers and rules, allowing you to create complex pricing strategies.

***

## How to Use the Product Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-product.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createProductStep = createStep(
  "create-product",
  async ({}, { container }) => {
    const productService = container.resolve(Modules.PRODUCT)

    const product = await productService.createProducts({
      title: "Medusa Shirt",
      options: [
        {
          title: "Color",
          values: ["Black", "White"],
        },
      ],
      variants: [
        {
          title: "Black Shirt",
          options: {
            Color: "Black",
          },
        },
      ],
    })

    return new StepResponse({ product }, product.id)
  },
  async (productId, { container }) => {
    if (!productId) {
      return
    }
    const productService = container.resolve(Modules.PRODUCT)

    await productService.deleteProducts([productId])
  }
)

export const createProductWorkflow = createWorkflow(
  "create-product",
  () => {
    const { product } = createProductStep()

    return new WorkflowResponse({
      product,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createProductWorkflow } from "../../workflows/create-product"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createProductWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createProductWorkflow } from "../workflows/create-product"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createProductWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createProductWorkflow } from "../workflows/create-product"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createProductWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Configure Selling Products

In this guide, you'll learn how to set up and configure your products based on their shipping and inventory requirements, their type, how you want to sell them, or your commerce ecosystem.

The concepts in this guide are applicable starting from [Medusa v2.5.1](https://github.com/medusajs/medusa/releases/tag/v2.5.1).

## Scenario

Businesses can have different selling requirements. They may sell:

1. Physical or digital items.
2. Items that don't require shipping or inventory management, such as selling digital products, services, or booking appointments.
3. Items whose inventory is managed by an external system, such as an ERP.

Medusa supports these different selling requirements by allowing you to configure shipping and inventory requirements for products and their variants.

This guide explains how these configurations work, then provides examples of setting up different use cases.

***

## Configuring Shipping Requirements

### Product Shipping Requirement

The Medusa application defines a link between the `Product` data model and the [ShippingProfile](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/concepts#shipping-profile/index.html.md) data model in the [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md), allowing you to associate a product with a shipping profile.

![Diagram showcasing the link between a product and its shipping profile](https://res.cloudinary.com/dza7lstvk/image/upload/v1752827367/Medusa%20Resources/product-shipping-requirement_cpfpkz.jpg)

When a product is associated with a shipping profile, its variants require shipping and fulfillment when purchased. This is useful for physical products or digital products that require custom fulfillment.

If a product doesn't have an associated shipping profile, its variants don't require shipping and fulfillment when purchased. This is useful for digital products, for example, that don't require shipping.

### Overriding Shipping Requirements for Variants

A product variant whose inventory is managed by Medusa (its `manage_inventory` property is enabled) has an [inventory item](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventoryitem/index.html.md).

The inventory item has a `requires_shipping` property that can be used to override the variant's shipping requirement. This is useful if the product has an associated shipping profile but you want to disable shipping for a specific variant, or vice versa.

![Diagram showcasing the link between a product variant and its inventory item, and the inventory item's shipping requirement](https://res.cloudinary.com/dza7lstvk/image/upload/v1752828341/Medusa%20Resources/product-variant-shipping-requirement_ux5y44.jpg)

Refer to the [Product Variant Inventory](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md) guide to learn more.

When a product variant is purchased, the Medusa application decides whether the purchased item requires shipping based on the following conditions (in the following order):

1. If the product variant has an inventory item, the Medusa application uses the inventory item's `requires_shipping` property to determine if the item requires shipping.
2. If the product variant doesn't have an inventory item, the Medusa application checks whether the product has an associated shipping profile to determine if the item requires shipping.

![Diagram showcasing the conditions that determine whether a product variant requires shipping](https://res.cloudinary.com/dza7lstvk/image/upload/v1752828969/Medusa%20Resources/shipping-requirement-check_fdh6lp.jpg)

### Shipping Requirements vs Shipping Options

The shipping options that you retrieve during checkout depend on the cart's shipping address. So, whether the items in the cart require shipping doesn't affect what shipping options are available at checkout.

This is a common misconception, where you expect to not receive any shipping options at checkout if the cart doesn't have any items that require shipping. However, the Medusa application always returns shipping options based on the cart's shipping address.

If you want to show the shipping options only if the cart has items that require shipping, you can either:

- Create a custom [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) that checks whether the cart has items that require shipping.
- Perform this check in your storefront's frontend code, such as in the checkout page, and show or hide the shipping options accordingly.

***

## Use Case Examples

By combining configurations of shipment requirements and inventory management, you can set up your products to support your use case:

|Use Case|Configurations|Example|
|---|---|---|---|---|
|Item that's shipped on purchase, and its variant inventory is managed by the Medusa application.||Any stock-kept item (clothing, for example), whose inventory is managed in the Medusa application.|
|Item that's shipped on purchase, but its variant inventory is managed externally (not by Medusa) or it has infinite stock.||Any stock-kept item (clothing, for example), whose inventory is managed in an ERP or has infinite stock.|
|Item that's not shipped on purchase, but its variant inventory is managed by Medusa.||Digital products, such as licenses, that don't require shipping but have a limited quantity.|
|Item that doesn't require shipping and its variant inventory isn't managed by Medusa.|||


# Product Variant Inventory

# Product Variant Inventory

In this guide, you'll learn about the inventory management features related to product variants.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/products/variants#manage-product-variant-inventory/index.html.md) to learn how to manage inventory of product variants.

## Configure Inventory Management of Product Variants

A product variant, represented by the [ProductVariant](https://docs.medusajs.com/references/product/models/ProductVariant/index.html.md) data model, has a `manage_inventory` field that's disabled by default. This field indicates whether you'll manage the inventory quantity of the product variant in the Medusa application. You can also keep `manage_inventory` disabled if you manage the product's inventory in an external system, such as an ERP.

The Product Module doesn't provide inventory-management features. Instead, the Medusa application uses the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) to manage inventory for products and variants. When `manage_inventory` is disabled, the Medusa application always considers the product variant to be in stock. This is useful if your product's variants aren't items that can be stocked, such as digital products, or they don't have a limited stock quantity.

When `manage_inventory` is enabled, the Medusa application tracks the inventory of the product variant using the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md). For example, when a customer purchases a product variant, the Medusa application decrements the stocked quantity of the product variant.

***

## How the Medusa Application Manages Inventory

When a product variant has `manage_inventory` enabled, the Medusa application creates an inventory item using the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) and links it to the product variant.

![Diagram showcasing the link between a product variant and its inventory item](https://res.cloudinary.com/dza7lstvk/image/upload/v1709652779/Medusa%20Resources/product-inventory_kmjnud.jpg)

The inventory item has one or more locations, called inventory levels, that represent the stock quantity of the product variant at a specific location. This allows you to manage inventory across multiple warehouses, such as a warehouse in the US and another in Europe.

![Diagram showcasing the link between a variant and its inventory item, and the inventory item's level.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738580390/Medusa%20Resources/variant-inventory-level_bbee2t.jpg)

Learn more about inventory concepts in the [Inventory Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md).

The Medusa application represents and manages stock locations using the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md). It creates a read-only link between the `InventoryLevel` and `StockLocation` data models so that it can retrieve the stock location of an inventory level.

![Diagram showcasing the read-only link between an inventory level and a stock location](https://res.cloudinary.com/dza7lstvk/image/upload/v1738582163/Medusa%20Resources/inventory-level-stock_amxfg5.jpg)

Learn more about the Stock Location Module in the [Stock Location Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/concepts/index.html.md).

### Product Inventory in Storefronts

When a storefront sends a request to the Medusa application, it must always pass a [publishable API key](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md) in the request header. This API key specifies the sales channels, available through the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md), of the storefront.

The Medusa application links sales channels to stock locations, indicating the locations available for a specific sales channel. So, all inventory-related operations are scoped by the sales channel and its associated stock locations.

For example, the availability of a product variant is determined by the `stocked_quantity` of its inventory level at the stock location linked to the storefront's sales channel.

![Diagram showcasing the overall relations between inventory, stock location, and sales channel concepts](https://res.cloudinary.com/dza7lstvk/image/upload/v1738582163/Medusa%20Resources/inventory-stock-sales_fknoxw.jpg)

***

## Variant Back Orders

Product variants have an `allow_backorder` field that's disabled by default. When enabled, the Medusa application allows customers to purchase the product variant even when it's out of stock. Use this when your product variant is available through on-demand or pre-order purchase.

You can also allow customers to subscribe to restock notifications of a product variant as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/commerce-automation/restock-notification/index.html.md).

***

## Additional Resources

The following guides provide more details on inventory management in the Medusa application:

- [Inventory Kits in the Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Learn how you can implement bundled or multi-part products through the Inventory Module.
- [Retrieve Product Variant Inventory Quantity](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/variant-inventory/index.html.md): Learn how to retrieve the available inventory quantity of a product variant.
- [Configure Selling Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/selling-products/index.html.md): Learn how to use inventory management to support different use cases when selling products.
- [Inventory in Flows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-in-flows/index.html.md): Learn how Medusa utilizes inventory management in different flows.
- [Storefront guide: how to retrieve a product variant's inventory details](https://docs.medusajs.com/resources/storefront-development/products/inventory/index.html.md).


# Promotion Actions

In this document, you’ll learn about promotion actions and how they’re computed using the [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md).

## computeActions Method

The Promotion Module's main service has a [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md) that returns an array of actions to perform on a cart when one or more promotions are applied.

Actions inform you what adjustment must be made to a cart item or shipping method. Each action is an object having the `action` property indicating the type of action.

***

## Action Types

### `addItemAdjustment` Action

The `addItemAdjustment` action indicates that an adjustment must be made to an item. For example, removing $5 off its amount.

This action has the following format:

```ts
export interface AddItemAdjustmentAction {
  action: "addItemAdjustment"
  item_id: string
  amount: number
  code: string
  description?: string
  is_tax_inclusive?: boolean
}
```

This action means that a new record should be created of the `LineItemAdjustment` data model in the Cart Module, or `OrderLineItemAdjustment` data model in the Order Module.

Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddItemAdjustmentAction/index.html.md) for details on the object’s properties.

### `removeItemAdjustment` Action

The `removeItemAdjustment` action indicates that an adjustment must be removed from a line item. For example, remove the $5 discount.

The `computeActions` method accepts any previous item adjustments in the `items` property of the second parameter.

This action has the following format:

```ts
export interface RemoveItemAdjustmentAction {
  action: "removeItemAdjustment"
  adjustment_id: string
  description?: string
  code: string
}
```

This action means that a new record should be removed of the `LineItemAdjustment` (or `OrderLineItemAdjustment`) with the specified ID in the `adjustment_id` property.

Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveItemAdjustmentAction/index.html.md) for details on the object’s properties.

### `addShippingMethodAdjustment` Action

The `addShippingMethodAdjustment` action indicates that an adjustment must be made on a shipping method. For example, make the shipping method free.

This action has the following format:

```ts
export interface AddShippingMethodAdjustment {
  action: "addShippingMethodAdjustment"
  shipping_method_id: string
  amount: number
  code: string
  description?: string
}
```

This action means that a new record should be created of the `ShippingMethodAdjustment` data model in the Cart Module, or `OrderShippingMethodAdjustment` data model in the Order Module.

Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddShippingMethodAdjustment/index.html.md) for details on the object’s properties.

### `removeShippingMethodAdjustment` Action

The `removeShippingMethodAdjustment` action indicates that an adjustment must be removed from a shipping method. For example, remove the free shipping discount.

The `computeActions` method accepts any previous shipping method adjustments in the `shipping_methods` property of the second parameter.

This action has the following format:

```ts
export interface RemoveShippingMethodAdjustment {
  action: "removeShippingMethodAdjustment"
  adjustment_id: string
  code: string
}
```

When the Medusa application receives this action type, it removes the `ShippingMethodAdjustment` (or `OrderShippingMethodAdjustment`) with the specified ID in the `adjustment_id` property.

Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveShippingMethodAdjustment/index.html.md) for details on the object’s properties.

### `campaignBudgetExceeded` Action

When the `campaignBudgetExceeded` action is returned, the promotions within a campaign can no longer be used as the campaign budget has been exceeded.

This action has the following format:

```ts
export interface CampaignBudgetExceededAction {
  action: "campaignBudgetExceeded"
  code: string
}
```

Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.CampaignBudgetExceededAction/index.html.md) for details on the object’s properties.


# Application Method

In this guide, you'll learn what an application method is in the Promotion Module.

## What is an Application Method?

The [ApplicationMethod data model](https://docs.medusajs.com/references/promotion/models/ApplicationMethod/index.html.md) defines how a promotion is applied. It has the following properties that determine its behavior:

|Property|Purpose|Possible Values|
|---|---|---|
|\`type\`|Does the promotion discount a fixed amount or a percentage?|\`fixed\`|
|\`target\_type\`|Is the promotion applied to a cart item, shipping method, or the entire order?|\`items\`|
|\`allocation\`|Is the discounted amount applied to each item, split between the applicable items, or applied on specific number of items?|\`each\`|

## Target Promotion Rules

When the promotion is applied to a cart item or a shipping method (in other words, when `target_type` is `items` or `shipping_methods`), you can restrict which items/shipping methods the promotion is applied to.

The `ApplicationMethod` data model has a collection of [PromotionRule](https://docs.medusajs.com/references/promotion/models/PromotionRule/index.html.md) records to restrict which items or shipping methods the promotion applies to. The `target_rules` property in the `ApplicationMethod` represents this relation.

![A diagram showcasing the target\_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898273/Medusa%20Resources/application-method-target-rules_hqaymz.jpg)

In this example, the promotion is only applied to product variants in the cart that have the SKU `SHIRT`.

***

## Buy Promotion Rules

When the promotion’s type is `buyget`, you must specify the “buy X” condition. For example, a cart must have two shirts before the promotion can be applied.

The application method has a collection of `PromotionRule` items to define the “buy X” rule. The `buy_rules` property in the `ApplicationMethod` represents this relation.

![A diagram showcasing the buy\_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898453/Medusa%20Resources/application-method-buy-rules_djjuhw.jpg)

In this example, the cart must have two product variants with the SKU `SHIRT` for the promotion to be applied.

***

## Maximum Quantity Restriction

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.

```json title="Example Cart"
{
  "cart": {
    "items": [
      {
        "id": "item_1",
        "quantity": 2 // The promotion is applied to 1 of this item
      }
    ]
  }
}
```

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.

```json title="Example Cart"
{
  "cart": {
    "items": [
      {
        "id": "item_1",
        "quantity": 2 // The promotion is applied to 1 of this item
      },
      {
        "id": "item_2",
        "quantity": 3 // The promotion is applied to 1 of this item
      }
    ]
  }
}
```

### Cart Level Restriction

The `once` allocation type is available from [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0).

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.


# Campaign

In this guide, you'll learn what a campaign is and its related concepts.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/campaigns/index.html.md) to learn how to manage campaigns using the dashboard.

## What is a Campaign?

A [Campaign](https://docs.medusajs.com/references/promotion/models/Campaign/index.html.md) 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.

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)

***

## 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 a promotion can be used.

There are three types of budgets: two that are global and one that is based on cart attributes.

### 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)

Global budgets track usage and limits through the following properties of the `CampaignBudget` data model:

- `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.

### Attribute-based Budgets

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

In this guide, you’ll learn about the main promotion and rule concepts in the Promotion Module.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard.

## What is a Promotion?

A promotion, represented by the [Promotion data model](https://docs.medusajs.com/references/promotion/models/Promotion/index.html.md), is a discount that can be applied on cart items, shipping methods, or entire orders.

A promotion has two types:

- `standard`: A standard promotion with rules.
- `buyget`: “A buy X get Y” promotion with rules.

|\`standard\`|\`buyget\`|
|---|---|
|A coupon code that gives customers 10% off their entire order.|Buy two shirts and get another for free.|
|A coupon code that gives customers $15 off any shirt in their order.|Buy two shirts and get 10% off the entire order.|
|A discount applied automatically for VIP customers that removes 10% off their shipping method’s amount.|Spend $100 and get free shipping.|

The Medusa Admin UI may not provide a way to create each of these promotion examples. However, they are supported by the Promotion Module and Medusa's workflows and API routes.

***

## Promotion Limits

The `limit` property is available since [Medusa v2.12.0](https://github.com/medusajs/medusa/releases/tag/v2.12.0).

A promotion can have usage limits to restrict how many times it can be used.

There are three ways to limit a promotion's usage:

1. By setting its `limit` property: This limits the total number of times the promotion can be used across all orders.
2. By setting the [global budget on the promotion's campaign](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/campaign#global-budgets/index.html.md): This limits the total spend or usage across all promotions in the campaign.
3. By setting the [attribute-based budget on the promotion's campaign](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/campaign#attribute-based-budgets/index.html.md): This limits the total spend or usage for specific attributes across all promotions in the campaign. For example, limiting the budget for a specific customer group.

All budgets are applied on the promotion. Once a budget is exhausted, the promotion can no longer be applied.

For example, if a promotion has a `limit` of `10` and its campaign has a global budget of `$1000`, the promotion can only be used `10` times or until the total discount given reaches `$1000`, whichever comes first.

***

## Promotion Rules

A promotion can be restricted by a set of rules, each rule is represented by the [PromotionRule data model](https://docs.medusajs.com/references/promotion/models/PromotionRule/index.html.md).

For example, you can create a promotion that only customers of the `VIP` customer group can use.

![A diagram showcasing the relation between Promotion and PromotionRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1709833196/Medusa%20Resources/promotion-promotion-rule_msbx0w.jpg)

A `PromotionRule`'s `attribute` property indicates the property's name to which this rule is applied. For example, `customer_group_id`.

The expected value for the attribute is stored in the `PromotionRuleValue` data model. So, a rule can have multiple values.

When testing whether a promotion can be applied to a cart, the rule's `attribute` property and its values are tested on the cart itself.

For example, the cart's customer must be part of the customer group(s) indicated in the promotion rule's value.

### Flexible Rules

The `PromotionRule`'s `operator` property adds more flexibility to the rule’s condition rather than simple equality (`eq`).

For example, to restrict the promotion to only `VIP` and `B2B` customer groups:

- Add a `PromotionRule` record with its `attribute` property set to `customer_group_id` and `operator` property to `in`.
- Add two `PromotionRuleValue` records associated with the rule: one with the value `VIP` and the other `B2B`.

![A diagram showcasing the relation between PromotionRule and PromotionRuleValue when a rule has multiple values](https://res.cloudinary.com/dza7lstvk/image/upload/v1709897383/Medusa%20Resources/promotion-promotion-rule-multiple_hctpmt.jpg)

In this case, a customer’s group must be in the `VIP` and `B2B` set of values to use the promotion.

***

## How to Apply Rules on a Promotion?

### Using Workflows

If you're managing promotions using [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) or the API routes that use them, you can specify rules for the promotion or its [application method](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/application-method/index.html.md).

For example, if you're creating a promotion using the [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md):

```ts
const { result } = await createPromotionsWorkflow(container)
  .run({
    input: {
      promotionsData: [{
        code: "10OFF",
        type: "standard",
        status: "active",
        application_method: {
          type: "percentage",
          target_type: "items",
          allocation: "across",
          value: 10,
          currency_code: "usd",
        },
        rules: [
          {
            attribute: "customer.group.id",
            operator: "eq",
            values: [
              "cusgrp_123",
            ],
          },
        ],
      }],
    },
  })
```

In this example, the promotion is restricted to customers with the `cusgrp_123` customer group.

### Using Promotion Module's Service

For most use cases, it's recommended to use [workflows](#using-workflows) instead of directly using the module's service.

If you're managing promotions using the Promotion Module's service, you can specify rules for the promotion or its [application method](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/application-method/index.html.md) in its methods.

For example, if you're creating a promotion with the [createPromotions](https://docs.medusajs.com/references/promotion/createPromotions/index.html.md) method:

```ts
const promotions = await promotionModuleService.createPromotions([
  {
    code: "50OFF",
    type: "standard",
    status: "active",
    application_method: {
      type: "percentage",
      target_type: "items",
      value: 50,
    },
    rules: [
      {
        attribute: "customer.group.id",
        operator: "eq",
        values: [
          "cusgrp_123",
        ],
      },
    ],
  },
])
```

In this example, the promotion is restricted to customers with the `cusgrp_123` customer group.

### How is the Promotion Rule Applied?

A promotion is applied on a resource if its attributes match the promotion's rules.

For example, consider you have the following promotion with a rule that restricts the promotion to a specific customer:

```json
{
  "code": "10OFF",
  "type": "standard",
  "status": "active",
  "application_method": {
    "type": "percentage",
    "target_type": "items",
    "allocation": "across",
    "value": 10,
    "currency_code": "usd"
  },
  "rules": [
    {
      "attribute": "customer_id",
      "operator": "eq",
      "values": [
        "cus_123"
      ]
    }
  ]
}
```

When you try to apply this promotion on a cart, the cart's `customer_id` is compared to the promotion rule's value based on the specified operator. So, the promotion will only be applied if the cart's `customer_id` is equal to `cus_123`.


# Links between Promotion Module and Other Modules

This document showcases the module links defined between the Promotion Module and other Commerce Modules.

## Summary

The Promotion Module has the following links to other modules:

Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|Cart|Promotion|Stored - many-to-many|Learn more|
|LineItemAdjustment|Promotion|Read-only - has one|Learn more|
|Order|Promotion|Stored - many-to-many|Learn more|

***

## Cart Module

A promotion can be applied on line items and shipping methods of a cart. Medusa defines a link between the `Cart` and `Promotion` data models.

![A diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg)

Medusa also defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItemAdjustment` data model and the `Promotion` data model. Because the link is read-only from the `LineItemAdjustment`'s side, you can only retrieve the promotion applied on a line item, and not the other way around.

### Retrieve with Query

To retrieve the carts that a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `carts.*` in `fields`:

To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`.

### query.graph

```ts
const { data: promotions } = await query.graph({
  entity: "promotion",
  fields: [
    "carts.*",
  ],
})

// promotions[0].carts
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: promotions } = useQueryGraphStep({
  entity: "promotion",
  fields: [
    "carts.*",
  ],
})

// promotions[0].carts
```

### Manage with Link

To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.PROMOTION]: {
    promotion_id: "promo_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.PROMOTION]: {
    promotion_id: "promo_123",
  },
})
```

***

## Order Module

An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models.

![A diagram showcasing an example of how data models from the Order and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716555015/Medusa%20Resources/order-promotion_dgjzzd.jpg)

### Retrieve with Query

To retrieve the orders a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`:

### query.graph

```ts
const { data: promotions } = await query.graph({
  entity: "promotion",
  fields: [
    "orders.*",
  ],
})

// promotions[0].orders
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: promotions } = useQueryGraphStep({
  entity: "promotion",
  fields: [
    "orders.*",
  ],
})

// promotions[0].orders
```

### Manage with Link

To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.PROMOTION]: {
    promotion_id: "promo_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.PROMOTION]: {
    promotion_id: "promo_123",
  },
})
```


# Promotion Module

In this section of the documentation, you will find resources to learn more about the Promotion Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard.

Medusa has promotion related features available out-of-the-box through the Promotion Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Promotion Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Promotion Features

- [Discount Functionalities](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts/index.html.md): A promotion discounts an amount or percentage of a cart's items, shipping methods, or the entire order.
- [Flexible Promotion Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts#flexible-rules/index.html.md): A promotion has rules that restricts when the promotion is applied.
- [Campaign Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/campaign/index.html.md): A campaign combines promotions under the same conditions, such as start and end dates, and budget configurations.
- [Apply Promotion on Carts and Orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md): Apply promotions on carts and orders to discount items, shipping methods, or the entire order.

***

## How to Use the Promotion Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-promotion.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createPromotionStep = createStep(
  "create-promotion",
  async ({}, { container }) => {
    const promotionModuleService = container.resolve(Modules.PROMOTION)

    const promotion = await promotionModuleService.createPromotions({
      code: "10%OFF",
      type: "standard",
      application_method: {
        type: "percentage",
        target_type: "order",
        value: 10,
        currency_code: "usd",
      },
    })

    return new StepResponse({ promotion }, promotion.id)
  },
  async (promotionId, { container }) => {
    if (!promotionId) {
      return
    }
    const promotionModuleService = container.resolve(Modules.PROMOTION)

    await promotionModuleService.deletePromotions(promotionId)
  }
)

export const createPromotionWorkflow = createWorkflow(
  "create-promotion",
  () => {
    const { promotion } = createPromotionStep()

    return new WorkflowResponse({
      promotion,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createPromotionWorkflow } from "../../workflows/create-cart"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createPromotionWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createPromotionWorkflow } from "../workflows/create-cart"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createPromotionWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createPromotionWorkflow } from "../workflows/create-cart"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createPromotionWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Tax-Inclusive Promotions

In this guide, you’ll learn how taxes are applied to promotions in a cart.

This feature is available from [Medusa v2.8.5](https://github.com/medusajs/medusa/releases/tag/v2.8.5).

## What are Tax-Inclusive Promotions?

By default, promotions are tax-exclusive, meaning that the discount amount is applied as-is to the cart before taxes are calculated and applied to the cart total.

A tax-inclusive promotion is a promotion for which taxes are calculated from the discount amount entered by the merchant.

When a promotion is tax-inclusive, the discount amount is reduced by the calculated tax amount based on the [tax region's rate](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-region/index.html.md). The reduced discount amount is then applied to the cart total.

Tax-inclusiveness doesn't apply to Buy X Get Y promotions.

### When to Use Tax-Inclusive Promotions

Tax-inclusive promotions are most useful when using [tax-inclusive prices](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md) for items in the cart.

In this scenario, Medusa applies taxes consistently across the cart, ensuring that the total price reflects the taxes and promotions correctly.

You can see this in action in the [examples below](#tax-inclusiveness-examples).

***

## What Makes a Promotion Tax-Inclusive?

The [Promotion data model](https://docs.medusajs.com/references/promotion/models/Promotion/index.html.md) has an `is_tax_inclusive` property that determines whether the promotion is tax-inclusive.

If `is_tax_inclusive` is disabled (which is the default), the promotion's discount amount will be applied as-is to the cart, before taxes are calculated. See an example in the [Tax-Exclusive Promotion Example](#tax-exclusive-promotion-example) section.

If `is_tax_inclusive` is enabled, the promotion's discount amount will first be reduced by the calculated tax amount (based on the [tax region's rate](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-region/index.html.md)). The reduced discount amount is then applied to the cart total. See an example in the [Tax-Inclusive Promotion Example](#tax-inclusive-promotion-example) section.

***

## How to Set a Promotion as Tax-Inclusive

You can enable tax-inclusiveness for a promotion when [creating it in the Medusa Admin](https://docs.medusajs.com/user-guide/promotions/create/index.html.md).

You can set the `is_tax_inclusive` property when creating a promotion by using either the [Promotion workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/workflows/index.html.md) or the [Promotion Module's service](https://docs.medusajs.com/references/promotion/index.html.md).

For most use cases, it's recommended to use [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) instead of directly using the module's service, as they implement the necessary rollback mechanisms in case of errors.

For example, if you're creating a promotion with the [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md) in an API route:

```ts highlights={[["17"]]}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createPromotionsWorkflow } from "@medusajs/medusa/core-flows"

export async function POST(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createPromotionsWorkflow(req.scope)
    .run({
      input: {
        promotionsData: [{
          code: "10OFF",
          // ...
          is_tax_inclusive: true,
        }],
      },
    })

  res.send(result)
}
```

In the above example, you set the `is_tax_inclusive` property to `true` when creating the promotion, making it tax-inclusive.

### Updating a Promotion's Tax-Inclusiveness

A promotion's tax-inclusiveness cannot be updated after it has been created. If you need to change a promotion's tax-inclusiveness, you must delete the existing promotion and create a new one with the desired `is_tax_inclusive` value.

***

## Tax-Inclusiveness Examples

The following sections provide examples of how tax-inclusive promotions work in different scenarios, including both tax-exclusive and tax-inclusive promotions.

These examples will help you understand how tax-inclusive promotions affect the cart total, allowing you to decide when to use them effectively.

### Tax-Exclusive Promotion Example

Consider the following scenario:

- A tax-exclusive promotion gives a `$10` discount on the cart's total.
- The cart's tax region has a `25%` tax rate.
- The cart total before applying the promotion is `$100`.
- [The prices in the cart's tax region are tax-exclusive](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md).

The result:

1. Apply `$10` discount to cart total: `$100` - `$10` = `$90`
2. Calculate tax on discounted total: `$90` x `25%` = `$22.50`
3. Final total: `$90` + `$22.50` = `$112.50`

### Tax-Inclusive Promotion Example

Consider the following scenario:

- A tax-inclusive promotion gives a `$10` discount on the cart's total.
- The cart's tax region has a `25%` tax rate.
- The cart total before applying the promotion is `$100`.
- [The prices in the cart's tax region are tax-exclusive](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md).

The result:

1. Calculate actual discount (removing tax): `$10` ÷ `1.25` = `$8`
2. Apply discount to cart total: `$100` - `$8` = `$92`
3. Calculate tax on discounted total: `$92` x `25%` = `$23`
4. Final total: `$92` + `$23` = `$115`

### Tax-Inclusive Promotions with Tax-Inclusive Prices

Consider the following scenario:

- A tax-inclusive promotion gives a `$10` discount on the cart's total.
- The cart's tax region has a `25%` tax rate.
- The cart total before applying the promotion is `$100`.
- [The prices in the cart's tax region are tax-inclusive](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md).

The result:

1. Calculate actual discount (removing tax): `$10` ÷ `1.25` = `$8`
2. Calculate cart total without tax: `$100` ÷ `1.25` = `$80`
3. Apply discount to cart total without tax: `$80` - `$8` = `$72`
4. Add tax back to total: `$72` x `1.25` = `$90`

The final total is `$90`, which correctly applies both the tax-inclusive promotion and tax-inclusive pricing.


# Links between Region Module and Other Modules

This document showcases the module links defined between the Region Module and other Commerce Modules.

## Summary

The Region Module has the following links to other modules:

Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|Cart|Region|Read-only - has one|Learn more|
|Order|Region|Read-only - has one|Learn more|
|Region|PaymentProvider|Stored - many-to-many|Learn more|

***

## Cart Module

Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `Region` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the region of a cart, and not the other way around.

### Retrieve with Query

To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`:

### query.graph

```ts
const { data: carts } = await query.graph({
  entity: "cart",
  fields: [
    "region.*",
  ],
})

// carts[0].region
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: [
    "region.*",
  ],
})

// carts[0].region
```

***

## Order Module

Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `Region` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the region of an order, and not the other way around.

### Retrieve with Query

To retrieve the region of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`:

### query.graph

```ts
const { data: orders } = await query.graph({
  entity: "order",
  fields: [
    "region.*",
  ],
})

// orders[0].region
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: orders } = useQueryGraphStep({
  entity: "order",
  fields: [
    "region.*",
  ],
})

// orders[0].region
```

***

## Payment Module

You can specify for each region which payment providers are available for use.

Medusa defines a module link between the `PaymentProvider` and the `Region` data models.

![A diagram showcasing an example of how resources from the Payment and Region modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711569520/Medusa%20Resources/payment-region_jyo2dz.jpg)

### Retrieve with Query

To retrieve the payment providers of a region with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_providers.*` in `fields`:

### query.graph

```ts
const { data: regions } = await query.graph({
  entity: "region",
  fields: [
    "payment_providers.*",
  ],
})

// regions[0].payment_providers
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: regions } = useQueryGraphStep({
  entity: "region",
  fields: [
    "payment_providers.*",
  ],
})

// regions[0].payment_providers
```

### Manage with Link

To manage the payment providers in a region, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.REGION]: {
    region_id: "reg_123",
  },
  [Modules.PAYMENT]: {
    payment_provider_id: "pp_stripe_stripe",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.REGION]: {
    region_id: "reg_123",
  },
  [Modules.PAYMENT]: {
    payment_provider_id: "pp_stripe_stripe",
  },
})
```


# Region Module

In this section of the documentation, you will find resources to learn more about the Region Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/regions/index.html.md) to learn how to manage regions using the dashboard.

Medusa has region related features available out-of-the-box through the Region Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Region Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

***

## Region Features

- [Region Management](https://docs.medusajs.com/references/region/models/Region/index.html.md): Manage regions in your store. You can create regions with different currencies and settings.
- [Multi-Currency Support](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has a currency. You can support multiple currencies in your store by creating multiple regions.
- [Different Settings Per Region](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has its own settings, such as what countries belong to a region or its tax settings.

***

## How to Use Region Module's Service

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-region.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createRegionStep = createStep(
  "create-region",
  async ({}, { container }) => {
    const regionModuleService = container.resolve(Modules.REGION)

    const region = await regionModuleService.createRegions({
      name: "Europe",
      currency_code: "eur",
    })

    return new StepResponse({ region }, region.id)
  },
  async (regionId, { container }) => {
    if (!regionId) {
      return
    }
    const regionModuleService = container.resolve(Modules.REGION)

    await regionModuleService.deleteRegions([regionId])
  }
)

export const createRegionWorkflow = createWorkflow(
  "create-region",
  () => {
    const { region } = createRegionStep()

    return new WorkflowResponse({
      region,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createRegionWorkflow } from "../../workflows/create-region"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createRegionWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createRegionWorkflow } from "../workflows/create-region"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createRegionWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createRegionWorkflow } from "../workflows/create-region"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createRegionWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Links between Sales Channel Module and Other Modules

This document showcases the module links defined between the Sales Channel Module and other Commerce Modules.

## Summary

The Sales Channel Module has the following links to other modules:

Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|ApiKey|SalesChannel|Stored - many-to-many|Learn more|
|Cart|SalesChannel|Read-only - has one|Learn more|
|Order|SalesChannel|Read-only - has one|Learn more|
|Product|SalesChannel|Stored - many-to-many|Learn more|
|SalesChannel|StockLocation|Stored - many-to-many|Learn more|

***

## API Key Module

A publishable API key allows you to easily specify the sales channel scope in a client request.

Medusa defines a link between the `ApiKey` and the `SalesChannel` data models.

![A diagram showcasing an example of how resources from the Sales Channel and API Key modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709812064/Medusa%20Resources/sales-channel-api-key_zmqi2l.jpg)

### Retrieve with Query

To retrieve the API keys associated with a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `publishable_api_keys.*` in `fields`:

### query.graph

```ts
const { data: salesChannels } = await query.graph({
  entity: "sales_channel",
  fields: [
    "publishable_api_keys.*",
  ],
})

// salesChannels[0].publishable_api_keys
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: salesChannels } = useQueryGraphStep({
  entity: "sales_channel",
  fields: [
    "publishable_api_keys.*",
  ],
})

// salesChannels[0].publishable_api_keys
```

### Manage with Link

To manage the sales channels of an API key, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.API_KEY]: {
    publishable_key_id: "apk_123",
  },
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.API_KEY]: {
    publishable_key_id: "apk_123",
  },
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
})
```

***

## Cart Module

Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `SalesChannel` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the sales channel of a cart, and not the other way around.

### Retrieve with Query

To retrieve the sales channel of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`:

### query.graph

```ts
const { data: carts } = await query.graph({
  entity: "cart",
  fields: [
    "sales_channel.*",
  ],
})

// carts[0].sales_channel
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: [
    "sales_channel.*",
  ],
})

// carts[0].sales_channel
```

***

## Order Module

Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `SalesChannel` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the sales channel of an order, and not the other way around.

### Retrieve with Query

To retrieve the sales channel of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`:

### query.graph

```ts
const { data: orders } = await query.graph({
  entity: "order",
  fields: [
    "sales_channel.*",
  ],
})

// orders.sales_channel
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: orders } = useQueryGraphStep({
  entity: "order",
  fields: [
    "sales_channel.*",
  ],
})

// orders.sales_channel
```

***

## Product Module

A product has different availability for different sales channels. Medusa defines a link between the `Product` and the `SalesChannel` data models.

![A diagram showcasing an example of how resources from the Sales Channel and Product modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709809833/Medusa%20Resources/product-sales-channel_t848ik.jpg)

A product can be available in more than one sales channel. You can retrieve only the products of a sales channel.

### Retrieve with Query

To retrieve the products of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`:

### query.graph

```ts
const { data: salesChannels } = await query.graph({
  entity: "sales_channel",
  fields: [
    "products.*",
  ],
})

// salesChannels[0].products
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: salesChannels } = useQueryGraphStep({
  entity: "sales_channel",
  fields: [
    "products.*",
  ],
})

// salesChannels[0].products
```

### Manage with Link

To manage the sales channels of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
})
```

***

## Stock Location Module

A stock location is associated with a sales channel. This scopes inventory quantities associated with that stock location by the associated sales channel.

Medusa defines a link between the `SalesChannel` and `StockLocation` data models.

![A diagram showcasing an example of how resources from the Sales Channel and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716796872/Medusa%20Resources/sales-channel-location_cqrih1.jpg)

### Retrieve with Query

To retrieve the stock locations of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`:

### query.graph

```ts
const { data: salesChannels } = await query.graph({
  entity: "sales_channel",
  fields: [
    "stock_locations.*",
  ],
})

// salesChannels[0].stock_locations
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: salesChannels } = useQueryGraphStep({
  entity: "sales_channel",
  fields: [
    "stock_locations.*",
  ],
})

// salesChannels[0].stock_locations
```

### Manage with Link

To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
  [Modules.STOCK_LOCATION]: {
    sales_channel_id: "sloc_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
  [Modules.STOCK_LOCATION]: {
    sales_channel_id: "sloc_123",
  },
})
```


# Sales Channel Module

In this section of the documentation, you will find resources to learn more about the Sales Channel Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/sales-channels/index.html.md) to learn how to manage sales channels using the dashboard.

Medusa has sales channel related features available out-of-the-box through the Sales Channel Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Sales Channel Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## What's a Sales Channel?

A sales channel indicates an online or offline channel that you sell products on.

Some use case examples for using a sales channel:

- Implement a B2B Ecommerce Store.
- Specify different products for each channel you sell in.
- Support omnichannel in your ecommerce store.

***

## Sales Channel Features

- [Sales Channel Management](https://docs.medusajs.com/references/sales-channel/models/SalesChannel/index.html.md): Manage sales channels in your store. Each sales channel has different meta information such as name or description, allowing you to easily differentiate between sales channels.
- [Product Availability](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Medusa uses the Product and Sales Channel modules to allow merchants to specify a product's availability per sales channel.
- [Cart and Order Scoping](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Carts, available through the Cart Module, are scoped to a sales channel. Paired with the product availability feature, you benefit from more features like allowing only products available in sales channel in a cart.
- [Inventory Availability Per Sales Channel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Medusa links sales channels to stock locations, allowing you to retrieve available inventory of products based on the specified sales channel.

***

## How to Use Sales Channel Module's Service

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-sales-channel.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createSalesChannelStep = createStep(
  "create-sales-channel",
  async ({}, { container }) => {
    const salesChannelModuleService = container.resolve(Modules.SALES_CHANNEL)

    const salesChannels = await salesChannelModuleService.createSalesChannels([
      {
        name: "B2B",
      },
      {
        name: "Mobile App",
      },
    ])

    return new StepResponse({ salesChannels }, salesChannels.map((sc) => sc.id))
  },
  async (salesChannelIds, { container }) => {
    if (!salesChannelIds) {
      return
    }
    const salesChannelModuleService = container.resolve(Modules.SALES_CHANNEL)

    await salesChannelModuleService.deleteSalesChannels(
      salesChannelIds
    )
  }
)

export const createSalesChannelWorkflow = createWorkflow(
  "create-sales-channel",
  () => {
    const { salesChannels } = createSalesChannelStep()

    return new WorkflowResponse({
      salesChannels,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createSalesChannelWorkflow } from "../../workflows/create-sales-channel"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createSalesChannelWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createSalesChannelWorkflow } from "../workflows/create-sales-channel"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createSalesChannelWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createSalesChannelWorkflow } from "../workflows/create-sales-channel"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createSalesChannelWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Publishable API Keys with Sales Channels

In this document, you’ll learn what publishable API keys are and how to use them with sales channels.

## Publishable API Keys with Sales Channels

A publishable API key, provided by the API Key Module, is a client key scoped to one or more sales channels.

When sending a request to a Store API route, you must pass a publishable API key in the header of the request:

```bash
curl http://localhost:9000/store/products \
  x-publishable-api-key: {your_publishable_api_key}
```

The Medusa application infers the associated sales channels and ensures that only data relevant to the sales channel are used.

***

## How to Create a Publishable API Key?

To create a publishable API key, either use the [Medusa Admin](https://docs.medusajs.com/user-guide/settings/developer/publishable-api-keys/index.html.md) or the [Admin API Routes](https://docs.medusajs.com/api/admin#publishable-api-keys).

***

## Access Sales Channels in Custom Store API Routes

If you create an API route under the `/store` prefix, you can access the sales channels associated with the request's publishable API key using the `publishable_key_context` property of the request object.

For example:

```ts
import { MedusaStoreRequest, MedusaResponse } from "@medusajs/framework/http"
import { getVariantAvailability } from "@medusajs/framework/utils"

export async function GET(
  req: MedusaStoreRequest,
  res: MedusaResponse
) {
  const query = req.scope.resolve("query")
  const sales_channel_ids = req.publishable_key_context.sales_channel_ids

  res.json({
    sales_channel_id: sales_channel_ids[0],
  })
}
```

In this example, you retrieve the scope's sales channel IDs using `req.publishable_key_context.sales_channel_ids`, whose value is an array of IDs.

You can then use these IDs based on your business logic. For example, you can retrieve the sales channels' details using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).

Notice that the request object's type is `MedusaStoreRequest` instead of `MedusaRequest` to ensure the availability of the `publishable_key_context` property.


# Stock Location Concepts

In this guide, you’ll learn about the main concepts in the Stock Location Module.

## Stock Location

A stock location, represented by the [StockLocation data model](https://docs.medusajs.com/references/stock-location-next/models/StockLocation/index.html.md), represents a location where stock items are kept. For example, a warehouse.

Medusa uses stock locations to provide inventory details, stored and managed by the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md), per location.

***

## StockLocationAddress

The [StockLocationAddress data model](https://docs.medusajs.com/references/stock-location-next/models/StockLocationAddress/index.html.md) belongs to the `StockLocation` data model. It provides more detailed information about the location, such as country code or street address.


# Links between Stock Location Module and Other Modules

This document showcases the module links defined between the Stock Location Module and other Commerce Modules.

## Summary

The Stock Location Module has the following links to other modules:

Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|FulfillmentSet|StockLocation|Stored - many-to-one|Learn more|
|FulfillmentProvider|StockLocation|Stored - many-to-many|Learn more|
|InventoryLevel|StockLocation|Read-only - has many|Learn more|
|SalesChannel|StockLocation|Stored - many-to-many|Learn more|

***

## Fulfillment Module

A fulfillment set can be conditioned to a specific stock location.

Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models.

![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1712567101/Medusa%20Resources/fulfillment-stock-location_nlkf7e.jpg)

Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location.

![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399492/Medusa%20Resources/fulfillment-provider-stock-location_b0mulo.jpg)

### Retrieve with Query

To retrieve the fulfillment sets of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `fulfillment_sets.*` in `fields`:

To retrieve the fulfillment providers, pass `fulfillment_providers.*` in `fields`.

### query.graph

```ts
const { data: stockLocations } = await query.graph({
  entity: "stock_location",
  fields: [
    "fulfillment_sets.*",
  ],
})

// stockLocations[0].fulfillment_sets
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: stockLocations } = useQueryGraphStep({
  entity: "stock_location",
  fields: [
    "fulfillment_sets.*",
  ],
})

// stockLocations[0].fulfillment_sets
```

### Manage with Link

To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.STOCK_LOCATION]: {
    stock_location_id: "sloc_123",
  },
  [Modules.FULFILLMENT]: {
    fulfillment_set_id: "fset_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.STOCK_LOCATION]: {
    stock_location_id: "sloc_123",
  },
  [Modules.FULFILLMENT]: {
    fulfillment_set_id: "fset_123",
  },
})
```

***

## Inventory Module

Medusa defines a read-only link between the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md)'s `InventoryLevel` data model and the `StockLocation` data model. Because the link is read-only from the `InventoryLevel`'s side, you can only retrieve the stock location of an inventory level, and not the other way around.

### Retrieve with Query

To retrieve the stock locations of an inventory level with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`:

### query.graph

```ts
const { data: inventoryLevels } = await query.graph({
  entity: "inventory_level",
  fields: [
    "stock_locations.*",
  ],
})

// inventoryLevels[0].stock_locations
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: inventoryLevels } = useQueryGraphStep({
  entity: "inventory_level",
  fields: [
    "stock_locations.*",
  ],
})

// inventoryLevels[0].stock_locations
```

***

## Sales Channel Module

A stock location is associated with a sales channel. This scopes inventory quantities in a stock location by the associated sales channel.

Medusa defines a link between the `SalesChannel` and `StockLocation` data models.

![A diagram showcasing an example of how resources from the Sales Channel and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716796872/Medusa%20Resources/sales-channel-location_cqrih1.jpg)

### Retrieve with Query

To retrieve the sales channels of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`:

### query.graph

```ts
const { data: stockLocations } = await query.graph({
  entity: "stock_location",
  fields: [
    "sales_channels.*",
  ],
})

// stockLocations[0].sales_channels
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: stockLocations } = useQueryGraphStep({
  entity: "stock_location",
  fields: [
    "sales_channels.*",
  ],
})

// stockLocations[0].sales_channels
```

### Manage with Link

To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
  [Modules.STOCK_LOCATION]: {
    sales_channel_id: "sloc_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
  [Modules.STOCK_LOCATION]: {
    sales_channel_id: "sloc_123",
  },
})
```


# Stock Location Module

In this section of the documentation, you will find resources to learn more about the Stock Location Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/index.html.md) to learn how to manage stock locations using the dashboard.

Medusa has stock location related features available out-of-the-box through the Stock Location Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Stock Location Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Stock Location Features

- [Stock Location Management](https://docs.medusajs.com/references/stock-location-next/models/index.html.md): Store and manage stock locations. Medusa links stock locations with data models of other modules that require a location, such as the [Inventory Module's InventoryLevel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/links-to-other-modules/index.html.md).
- [Address Management](https://docs.medusajs.com/references/stock-location-next/models/StockLocationAddress/index.html.md): Manage the address of each stock location.

***

## How to Use Stock Location Module's Service

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-stock-location.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createStockLocationStep = createStep(
  "create-stock-location",
  async ({}, { container }) => {
    const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION)

    const stockLocation = await stockLocationModuleService.createStockLocations({
      name: "Warehouse 1",
    })

    return new StepResponse({ stockLocation }, stockLocation.id)
  },
  async (stockLocationId, { container }) => {
    if (!stockLocationId) {
      return
    }
    const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION)

    await stockLocationModuleService.deleteStockLocations([stockLocationId])
  }
)

export const createStockLocationWorkflow = createWorkflow(
  "create-stock-location",
  () => {
    const { stockLocation } = createStockLocationStep()

    return new WorkflowResponse({ stockLocation })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createStockLocationWorkflow } from "../../workflows/create-stock-location"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createStockLocationWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createStockLocationWorkflow } from "../workflows/create-stock-location"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createStockLocationWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createStockLocationWorkflow } from "../workflows/create-stock-location"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createStockLocationWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Links between Store Module and Other Modules

This document showcases the module links defined between the Store Module and other Commerce Modules.

## Summary

The Store Module has the following links to other modules:

Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|StoreCurrency|Currency|Read-only - has many|Learn more|
|StoreLocale|Locale|Read-only - has many|Learn more|

***

## Currency Module

The Store Module has a `StoreCurrency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol.

Instead, Medusa defines a read-only link from the Store Module's `StoreCurrency` data model to the [Currency Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/index.html.md)'s `Currency` data model. This means you can retrieve the details of a store's supported currencies, but you don't manage the links in a pivot table in the database.

### Retrieve with Query

To retrieve the details of a store's currencies with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_currencies.currency.*` in `fields`:

### query.graph

```ts
const { data: stores } = await query.graph({
  entity: "store",
  fields: [
    "supported_currencies.currency.*",
  ],
})

// stores[0].supported_currencies
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: stores } = useQueryGraphStep({
  entity: "store",
  fields: [
    "supported_currencies.currency.*",
  ],
})

// stores[0].supported_currencies
```

***

## Translation Module

### Prerequisites

- [Translation Module Configured](https://docs.medusajs.com/commerce-modules/translation#configure-translation-module/index.html.md)

The Store Module has a `StoreLocale` data model that stores the supported locales of a store. However, these locales don't hold all the details of a locale, such as its name.

Instead, Medusa defines a read-only link from the Store Module's `StoreLocale` data model to the [Translation Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/translation/index.html.md)'s `Locale` data model. This means you can retrieve the details of a store's supported locales, but you don't manage the links in a pivot table in the database.

### Retrieve with Query

To retrieve the details of a store's locales with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_locales.locale.*` in `fields`:

### query.graph

```ts
const { data: stores } = await query.graph({
  entity: "store",
  fields: [
    "supported_locales.locale.*",
  ],
})

// stores[0].supported_locales[0].locale
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: stores } = useQueryGraphStep({
  entity: "store",
  fields: [
    "supported_locales.locale.*",
  ],
})

// stores[0].supported_locales[0].locale
```


# Store Locales

In this guide, you'll learn about locales defined in the Store Module.

### Prerequisites

- [Medusa v2.12.3 or later](https://github.com/medusajs/medusa/releases/tag/v2.12.3)

While the Store Module allows you to manage the locales for your store, the actual locale data and translations are managed by the [Translation Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/translation/index.html.md).

## Supported Locales

The Store Module has a [StoreLocale](https://docs.medusajs.com/references/store/models/StoreLocale/index.html.md) data model that represents the locales supported by your store. It has a `locale_code` property that follows the [IETF BCP 47 standard](https://gist.github.com/typpo/b2b828a35e683b9bf8db91b5404f1bd1). For example, `en-US` represents American English, while `fr-FR` represents French (France).

`StoreLocale` belongs to the [Store](https://docs.medusajs.com/references/store/models/Store/index.html.md) data model, which has a `supported_locales` property that lists all the locales available in your store.

For example, if your store supports English (United States) and French (France), you'll have two `StoreLocale` records with the locale codes `en-US` and `fr-FR`.

![Diagram illustrating the relationship between the Store and StoreLocale data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1765371144/Medusa%20Resources/store-locale_ckanv4.jpg)


# Store Module

In this section of the documentation, you will find resources to learn more about the Store Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/store/index.html.md) to learn how to manage your store using the dashboard.

Medusa has store related features available out-of-the-box through the Store Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Store Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Store Features

- [Store Management](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create and manage stores in your application.
- [Multi-Tenancy Support](https://docs.medusajs.com/references/store/models/Store/index.html.md): While Medusa doesn't natively support multi-tenancy, the Store Module allows you to create and manage multiple stores within a single Medusa instance. You can then build customizations to link products, customers, orders, and other resources to specific stores.

***

## How to Use Store Module's Service

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-store.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createStoreStep = createStep(
  "create-store",
  async ({}, { container }) => {
    const storeModuleService = container.resolve(Modules.STORE)

    const store = await storeModuleService.createStores({
      name: "My Store",
      supported_currencies: [{
        currency_code: "usd",
        is_default: true,
      }],
    })

    return new StepResponse({ store }, store.id)
  },
  async (storeId, { container }) => {
    if(!storeId) {
      return
    }
    const storeModuleService = container.resolve(Modules.STORE)
    
    await storeModuleService.deleteStores([storeId])
  }
)

export const createStoreWorkflow = createWorkflow(
  "create-store",
  () => {
    const { store } = createStoreStep()

    return new WorkflowResponse({ store })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createStoreWorkflow } from "../../workflows/create-store"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createStoreWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createStoreWorkflow } from "../workflows/create-store"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createStoreWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createStoreWorkflow } from "../workflows/create-store"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createStoreWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Tax Module Options

In this guide, you'll learn about the options of the Tax Module.

## providers

The `providers` option is an array of either [tax module providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) or path to a file that defines a tax provider.

When the Medusa application starts, these providers are registered and can be used to retrieve tax lines.

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/tax",
      options: {
        providers: [
          {
            resolve: "./path/to/my-provider",
            id: "my-provider",
            options: {
              // ...
            },
          },
        ],
      },
    },
  ],
})
```

The objects in the array accept the following properties:

- `resolve`: A string indicating the package name of the module provider or the path to it.
- `id`: A string indicating the provider's unique name or ID.
- `options`: An optional object of the module provider's options.


# Tax Module

In this section of the documentation, you will find resources to learn more about the Tax Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard.

Medusa has tax related features available out-of-the-box through the Tax Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Tax Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Tax Features

- [Tax Settings Per Region](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-region/index.html.md): Set different tax settings for each tax region.
- [Tax Rates and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/index.html.md): Manage each region's default tax rates and override them with conditioned tax rates.
- [Retrieve Tax Lines for carts and orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/index.html.md): Calculate and retrieve the tax lines of a cart or order's line items and shipping methods with tax providers.
- [Custom Tax Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md): Create custom tax providers to calculate tax lines differently for each tax region.

***

## How to Use Tax Module's Service

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-tax-region.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createTaxRegionStep = createStep(
  "create-tax-region",
  async ({}, { container }) => {
    const taxModuleService = container.resolve(Modules.TAX)

    const taxRegion = await taxModuleService.createTaxRegions({
      country_code: "us",
    })

    return new StepResponse({ taxRegion }, taxRegion.id)
  },
  async (taxRegionId, { container }) => {
    if (!taxRegionId) {
      return
    }
    const taxModuleService = container.resolve(Modules.TAX)

    await taxModuleService.deleteTaxRegions([taxRegionId])
  }
)

export const createTaxRegionWorkflow = createWorkflow(
  "create-tax-region",
  () => {
    const { taxRegion } = createTaxRegionStep()

    return new WorkflowResponse({ taxRegion })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createTaxRegionWorkflow } from "../../workflows/create-tax-region"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createTaxRegionWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createTaxRegionWorkflow } from "../workflows/create-tax-region"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createTaxRegionWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createTaxRegionWorkflow } from "../workflows/create-tax-region"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createTaxRegionWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***

## Configure Tax Module

The Tax Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/module-options/index.html.md) for details on the module's options.

***


# Tax Calculation with the Tax Provider

In this guide, you’ll learn how tax lines are calculated using the tax provider.

## Tax Lines Calculation

Tax lines are calculated and retrieved using the [getTaxLines method of the Tax Module’s main service](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md). It accepts an array of line items and shipping methods, and the context of the calculation.

For example:

```ts
const taxLines = await taxModuleService.getTaxLines(
  [
    {
      id: "cali_123",
      product_id: "prod_123",
      unit_price: 1000,
      quantity: 1,
    },
    {
      id: "casm_123",
      shipping_option_id: "so_123",
      unit_price: 2000,
    },
  ],
  {
    address: {
      country_code: "us",
    },
  }
)
```

The context object is used to determine which tax regions and rates to use in the calculation. It includes properties related to the address and customer.

The example above retrieves the tax lines based on the tax region for the United States.

The method returns tax lines for the line item and shipping methods. For example:

```json
[
  {
    "line_item_id": "cali_123",
    "rate_id": "txr_1",
    "rate": 10,
    "code": "XXX",
    "name": "Tax Rate 1"
  },
  {
    "shipping_line_id": "casm_123",
    "rate_id": "txr_2",
    "rate": 5,
    "code": "YYY",
    "name": "Tax Rate 2"
  }
]
```

***

## Using the Tax Provider in the Calculation

The tax lines retrieved by the `getTaxLines` method are actually retrieved from the tax region’s [Tax Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md).

A tax module implements the logic to shape tax lines. Each tax region uses a tax provider.

Learn more about tax providers, configuring, and creating them in the [Tax Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) guide.


# Tax Module Provider

In this guide, you’ll learn about the Tax Module Provider and how it's used.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax provider of a tax region using the dashboard.

## What is a Tax Module Provider?

The Tax Module Provider handles tax line calculations in the Medusa application. It integrates third-party tax services, such as TaxJar, or implements custom tax calculation logic.

The Medusa application uses the Tax Module Provider whenever it needs to calculate tax lines for a cart or order, or when you [calculate the tax lines using the Tax Module's service](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/index.html.md).

![Diagram showcasing the communication between Medusa the Tax Module Provider, and the third-party tax provider.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746790996/Medusa%20Resources/tax-provider-service_kcgpne.jpg)

### Default Tax Provider

The Tax Module provides a `system` tax provider that acts as a placeholder tax provider. It performs basic tax calculation, as you can see in the [Create Tax Module Provider](https://docs.medusajs.com/references/tax/provider#gettaxlines/index.html.md) guide.

This provider is installed by default in your application and you can use it with tax regions.

The identifier of the system tax provider is `tp_system`.

### Other Tax Providers

- [Avalara](https://docs.medusajs.com/integrations/guides/avalara/index.html.md)

***

## How to Create a Custom Tax Provider?

A Tax Module Provider is a module whose service implements the `ITaxProvider` imported from `@medusajs/framework/types`.

The module can have multiple tax provider services, where each are registered as separate tax providers.

Refer to the [Create Tax Module Provider](https://docs.medusajs.com/references/tax/provider/index.html.md) guide to learn how to create a Tax Module Provider.

After you create a tax provider, you can choose it as the default Tax Module Provider for a region in the [Medusa Admin dashboard](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md).

***

## How are Tax Providers Registered?

### Configure Tax Module's Providers

The Tax Module accepts a `providers` option that allows you to configure the providers registered in your application.

Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/module-options/index.html.md) guide.

### Registration on Application Start

When the Medusa application starts, it registers the Tax Module Providers defined in the `providers` option of the Tax Module.

For each Tax Module Provider, the Medusa application finds all tax provider services defined in them to register.

### TaxProvider Data Model

A registered tax provider is represented by the [TaxProvider data model](https://docs.medusajs.com/references/tax/models/TaxProvider/index.html.md) in the Medusa application.

This data model is used to reference a service in the Tax Module Provider and determine whether it's installed in the application.

![Diagram showcasing the TaxProvider data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1746791254/Medusa%20Resources/tax-provider-model_r6ktjw.jpg)

The `TaxProvider` data model has the following properties:

- `id`: The unique identifier of the tax provider. The ID's format is `tp_{identifier}_{id}`, where:
  - `identifier` is the value of the `identifier` property in the Tax Module Provider's service.
  - `id` is the value of the `id` property of the Tax Module Provider in `medusa-config.ts`.
- `is_enabled`: A boolean indicating whether the tax provider is enabled.

### How to Remove a Tax Provider?

You can remove a registered tax provider from the Medusa application by removing it from the `providers` option in the Tax Module's configuration.

Then, the next time the Medusa application starts, it will set the `is_enabled` property of the `TaxProvider`'s record to `false`. This allows you to re-enable the tax provider later if needed by adding it back to the `providers` option.


# Tax Rates and Rules

In this guide, you’ll learn about tax rates and rules.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions#manage-tax-rate-overrides/index.html.md) to learn how to manage tax rates using the dashboard.

## What are Tax Rates?

A tax rate, represented by the [TaxRate data model](https://docs.medusajs.com/references/tax/models/TaxRate/index.html.md), is a percentage amount used to calculate the tax amount for each taxable item’s price, such as line items or shipping methods, in a cart. The sum of all calculated tax amounts are then added to the cart’s total as a tax total.

Each tax region has a default tax rate. This tax rate is applied to all taxable items of a cart in that region.

The `TaxRate` data model has a `rate` property that represents the percentage amount. For example, a `rate` of `10` means a tax rate of 10%.

### Combinable Tax Rates

Tax regions can have parent tax regions. To inherit the tax rates of the parent tax region, set the `is_combinable` of the child’s tax rates to `true`.

Then, when tax rates are retrieved for a taxable item in the child region, both the child and the parent tax regions’ applicable rates are returned.

***

## Override Tax Rates with Rules

You can create tax rates that override the default for specific conditions or rules.

For example, you can have a default 10% tax rate, but for products of type “Shirt” it is 15%.

A tax region can have multiple tax rates, and each tax rate can have multiple tax rules. The [TaxRateRule data model](https://docs.medusajs.com/references/tax/models/TaxRateRule/index.html.md) represents a tax rate’s rule.

![A diagram showcasing the relation between TaxRegion, TaxRate, and TaxRateRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1711462167/Medusa%20Resources/tax-rate-rule_enzbp2.jpg)

The following two properties of the data model identify the rule’s target:

- `reference`: the name of the table in the database that this rule points to. For example, `product_type`.
- `reference_id`: the ID of the data model’s record that this points to. For example, a product type’s ID.

So, to override the default tax rate for product types “Shirt”, you create a tax rate and associate with it a tax rule whose `reference` is `product_type` and `reference_id` the ID of the “Shirt” product type.


# Tax Region

In this document, you’ll learn about tax regions and how to use them with the Region Module.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard.

## What is a Tax Region?

A tax region, represented by the [TaxRegion data model](https://docs.medusajs.com/references/tax/models/TaxRegion/index.html.md), stores tax settings related to a region that your store serves.

Tax regions can inherit settings and rules from a parent tax region.

***

## Tax Rules in a Tax Region

Tax rules define the tax rates and behavior within a tax region. They specify:

- The tax rate percentage.
- Which products the tax applies to.
- Other custom rules to determine tax applicability.

Learn more about tax rules in the [Tax Rates and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/index.html.md) guide.

***

## Tax Provider

Each tax region can have a default tax provider. The tax provider is responsible for calculating the tax lines for carts and orders in that region.

You can use Medusa's default tax provider or create a custom one, allowing you to integrate with third-party tax services or implement your own tax calculation logic.

Learn more about tax providers in the [Tax Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) guide.


# Translation Concepts

In this guide, you'll learn about the key concepts related to the Translation Module, including locales and translations.

### Prerequisites

- [Translation Module Configured](https://docs.medusajs.com/commerce-modules/translation#configure-translation-module/index.html.md)

## Locales

The [Locale data model](https://docs.medusajs.com/references/translation/models/Locale/index.html.md) represents a language that resources can be translated into.

Its `code` property follows the [IETF BCP 47 standard](https://gist.github.com/typpo/b2b828a35e683b9bf8db91b5404f1bd1). For example, `en-US` represents American English, while `fr-FR` represents French (France).

Locales are automatically populated in your Medusa application the first time the Translation Module is used.

### Default Locale

The Translation Module doesn't enforce a default locale. If a resource doesn't have a translation in the requested locale, it will fall back to the original value stored in the resource's data model.

***

## Translations

The [Translation data model](https://docs.medusajs.com/references/translation/models/Translation/index.html.md) represents a translated value for a specific resource in a specific locale.

The data model has the following properties to associate the translation with the resource:

- `reference_id`: The ID of the resource being translated (for example, the ID of the product being translated).
- `reference`: The name of the table where the resource is stored (for example, `product` when translating a product).

The locale of the translation is indicated by the `locale_code` property, which follows the same [IETF BCP 47 standard](https://gist.github.com/typpo/b2b828a35e683b9bf8db91b5404f1bd1) as the [Locale](#locales) data model.

A resource may have only one translation per locale. For example, a product can have only one translation in `fr-FR` and another translation in `es-ES`.

The actual translated values are stored in the `translations` property, which is a JSON object where each key represents a field of the resource being translated, and the value is the translated text.

For example, a translation for a product to French (`fr-FR`) may look like this:

```json
{
  "reference_id": "prod_123",
  "reference": "product",
  "locale_code": "fr-FR",
  "translations": {
    "title": "Produit Exemple",
    "description": "Ceci est une description en français."
  }
}
```

Each key in the `translations` object corresponds to a property in the product resource, such as `title` and `description`, with their respective translated values.

***

## Retrieve Translations in Medusa

To retrieve translations for the storefront, refer to the [Serve Translations in the Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/translation/storefront/index.html.md) guide.

You can retrieve translations for a resource on the Medusa server using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). You can filter translations by `reference_id` and `locale_code` to get the specific translation you need.

For example:

```ts
const { data: translations } = await query.graph({
  entity: "translation",
  fields: ["reference_id", "locale_code", "translations"],
  filters: {
    reference_id: "prod_123",
    locale_code: "fr-FR",
  },
})

console.log(translations)
// Output:
// [{
//   reference_id: "prod_123",
//   locale_code: "fr-FR",
//   translations: {
//     title: "Produit Exemple",
//     description: "Ceci est une description en français."
//   }
// }]
```

In this example, you retrieve the translation records of a product with the ID `prod_123` in French (`fr-FR`).

If you've enabled the [Caching Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching/index.html.md) in your Medusa application, you can also cache translation queries to improve performance:

```ts
const { data: translations } = await query.graph({
  entity: "translation",
  fields: ["reference_id", "locale_code", "translations"],
  filters: {
    reference_id: "prod_123",
    locale_code: "fr-FR",
  },
}, {
  cache: {
    enable: true,
  },
})

console.log(translations)
// Output:
// [{
//   reference_id: "prod_123",
//   locale_code: "fr-FR",
//   translations: {
//     title: "Produit Exemple",
//     description: "Ceci est une description en français."
//   }
// }]
```


# Translate Custom Data Models

In this chapter, you'll learn how to support translations for your custom data models using the Translation Module.

### Prerequisites

- [Medusa v2.12.4 or later](https://github.com/medusajs/medusa/releases/tag/v2.12.4)
- [Translation Module Configured](https://docs.medusajs.com/commerce-modules/translation#configure-translation-module/index.html.md)

## Summary

The Translation Module allows you to extend translation capabilities to custom data models in your Medusa application. Then, you can [manage translations from the Medusa Admin](https://docs.medusajs.com/user-guide/settings/translations/index.html.md), and serve translated resources in your configured locales.

By following this guide, you'll learn how to:

- Configure the Translation Module to support translations for your custom data models.
- Manage translations for your custom data models from the Medusa Admin.
- Serve translated resources in your configured locales.

***

## Prerequisites: Custom Data Model

This guide assumes you already have a custom [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) with a data model. The guide will use a Blog Module with the following `Post` data model as an example:

```ts title="src/modules/blog/models/post.ts"
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id().primaryKey(),
  title: model.text(),
})

export default Post
```

The module must also be registered in `medusa-config.ts`. For example:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    // other modules...
    {
      resolve: "./src/modules/blog",
    },
  ],
})
```

***

## Step 1: Configure Translatable Entities

The first step is to configure the Translation Module with the custom entities you want to support translations for.

Before proceeding, run the `build` command to ensure the generated types are up-to-date:

```bash npm2yarn
npm run build
```

This will allow you to benefit from auto-completion when configuring the Translation Module.

Next, in `medusa-config.ts`, add the `options.entities` property to the Translation Module configuration:

```ts title="medusa-config.ts" highlights={configHighlights}
module.exports = defineConfig({
  // ...
  modules: [
    // other modules...
    {
      resolve: "@medusajs/medusa/translation",
      options: {
        entities: [
          {
            type: "post",
            fields: ["title"],
          },
        ],
      },
    },
  ],
})
```

The `options.entities` option is an array of objects indicating the custom data models to support translations for. Each object has the following properties:

1. `type`: The name of the table where the custom data model is stored. This is the same value passed as the first parameter to `model.define`.
2. `fields`: An array of fields in the custom data model to support translations for.

***

## Step 2: Manage Translations from Medusa Admin

After configuring the Translation Module, you can manage translations for your custom data models from the Medusa Admin.

Run the following command to start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin and go to Settings -> Translations. You should see your custom data model in the list of translatable resources.

![Post data model highlighted in the list of translatable resources in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1767687396/Medusa%20Resources/CleanShot_2026-01-06_at_10.12.09_2x_w1hmua.png)

Click the Edit button for your custom data model to manage translations for its resources. You can manage translations for the configured locales and fields.

Learn more in the [Translations User Guide](https://docs.medusajs.com/user-guide/settings/translations/index.html.md).

***

## Step 3: Serve Translated Resources

Finally, you can serve translated resources for your custom data models in your configured locales. This section focuses on returning records with translated fields from API routes.

### Pass Locale in API Requests

Medusa supports passing the desired locale in API requests to `/store` routes using either:

1. The `locale` query parameter.
2. The `x-medusa-locale` header.

For example, assuming you have a `/store/posts` API route, you can pass the locale in the request as follows:

### Query Parameter

```bash
curl "http://localhost:9000/store/posts?locale=fr-FR" \
-H 'x-publishable-api-key: {your_publishable_api_key}'
```

### Request Header

```bash
curl "http://localhost:9000/store/posts" \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
-H 'x-medusa-locale: fr-FR'
```

You must pass a publishable API key in the request header to store API routes. Learn more in the [Store API reference](https://docs.medusajs.com/api/store#publishable-api-key).

If your API route isn't under the `/store` prefix, you must apply the `applyLocale` middleware. For example, add the middleware to the `src/api/middlewares.ts` file:

```ts title="src/api/middlewares.ts"
import { applyLocale, defineMiddlewares } from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/posts",
      middlewares: [applyLocale],
    },
  ],
})
```

This allows you to pass the locale in the query parameter or request header for the `/posts` API route.

### Handle Translations in API Routes

In your custom API routes, you can retrieve the request's locale from the `locale` property of the `MedusaRequest` object. Pass that property to [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve your data models with translated fields.

For example, to retrieve blog posts with translated titles in the `/store/posts` API route:

```ts title="src/api/routes/store/posts.ts" highlights={apiRouteHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const query = req.scope.resolve("query")

  const { data: posts } = await query.graph(
    {
      entity: "post",
      fields: ["id", "title"],
    },
    {
      locale: req.locale,
    }
  )

  res.json({ posts })
}
```

In this example, the `locale` option is set to `req.locale`. Medusa will set the `title` field of each post to its translated value if a translation is available for the requested locale. Otherwise, it returns the original value stored in the data model.

***

## Step 4: Test the Implementation

To test the implementation, start the Medusa application if you haven't already:

```bash npm2yarn
npm run dev
```

Then, send a request to your custom API route with the desired locale. For example:

```bash
curl "http://localhost:9000/store/posts?locale=fr-FR" \
-H 'x-publishable-api-key: {your_publishable_api_key}'
```

This should return the list of blog posts with their French translations for the `title` field if available:

```json
{
  "posts": [
    {
      "id": "post_123",
      "title": "Titre de l'Article"
    },
    {
      "id": "post_456",
      "title": "Un Autre Titre"
    }
  ]
}
```

***

## Pass Locale to useQueryGraphStep

If your API route executes a workflow and returns its result, you can retrieve data models with translated fields in the workflow by passing the locale to `useQueryGraphStep`.

For example:

```ts title="src/workflows/handle-posts.ts" highlights={workflowHighlights}
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

type WorkflowInput = {
  locale: string;
}

export const handlePostsWorkflow = createWorkflow(
  "handle-posts",
  (input: WorkflowInput) => {
    // do something...
    
    const { data: posts } = useQueryGraphStep({
      entity: "post",
      fields: ["id", "title"],
      options: {
        locale: input.locale,
      },
    })

    return new WorkflowResponse({
      posts,
    })
  }
)
```

In this example, the workflow accepts a `locale` input parameter. You then retrieve the posts with translated titles by passing the `locale` input to `useQueryGraphStep`.

The returned posts will have their `title` field set to the translated value if a translation is available for the requested locale.

You can execute the workflow in your custom API routes and pass the request locale as an input parameter:

```ts title="src/api/routes/store/posts.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { handlePostsWorkflow } from "../../workflows/handle-posts"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await handlePostsWorkflow(req.scope)
    .run({
      input: {
        locale: req.locale,
      },
    })

  res.json(result)
}
```

The returned posts in the API response will have their `title` field set to the translated value if a translation is available for the requested locale.

***

## Manage Translations with the Translation Module Service

For more complex cases, you can manage translations for your custom data models programmatically using the Translation Module's service. You can create, update, delete, and retrieve translations of any resource using the service's methods.

Refer to the [Translation Module service reference](https://docs.medusajs.com/references/translation/index.html.md) for a full list of available methods and how to use them.


# Links between Translation Module and Other Modules

This document showcases the module links that Medusa defines between the Translation Module and other Commerce Modules.

## Summary

Medusa defines the following links between the Translation Module and other Commerce Modules:

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|Product|Translation|Read-only - one-to-many|Learn more|
|ProductVariant|Translation|Read-only - one-to-many|Learn more|
|ProductCategory|Translation|Read-only - one-to-many|Learn more|
|ProductCollection|Translation|Read-only - one-to-many|Learn more|
|ProductTag|Translation|Read-only - one-to-many|Learn more|
|ProductType|Translation|Read-only - one-to-many|Learn more|
|ProductOption|Translation|Read-only - one-to-many|Learn more|
|ProductOptionValue|Translation|Read-only - one-to-many|Learn more|
|StoreLocale|Locale|Read-only - has many|Learn more|

***

## Product Module

Medusa defines the following read-only links between the Translation and Product Modules:

|Data Model|Link Type|Description|
|---|---|---|
|Product|One-to-many (bidirectional)|Retrieve translations associated with a product and vice versa.|
|ProductVariant|One-to-many (read-only)|Retrieve translations of a product variant only.|
|ProductCategory|One-to-many (read-only)|Retrieve translations of a product category only.|
|ProductCollection|One-to-many (read-only)|Retrieve translations of a product collection only.|
|ProductTag|One-to-many (read-only)|Retrieve translations of a product tag only.|
|ProductType|One-to-many (read-only)|Retrieve translations of a product type only.|
|ProductOption|One-to-many (read-only)|Retrieve translations of a product option only.|
|ProductOptionValue|One-to-many (read-only)|Retrieve translations of a product option value only.|

### Retrieve with Query

To retrieve the translations of a product with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `translations.*` in `fields`:

You can pass the `translations.*` field when querying any of the above-mentioned data models to retrieve their associated translations.

### query.graph

```ts
const { data: products } = await query.graph({
  entity: "product",
  fields: [
    "translations.*",
  ],
})

// products[0].translations
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: products } = useQueryGraphStep({
  entity: "product",
  fields: [
    "translations.*",
  ],
})

// products[0].translations
```

***

## Store Module

The [Store Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/store/index.html.md) has a `StoreLocale` data model that stores the supported locales of a store. However, these locales don't hold all the details of a locale, such as its name.

Instead, Medusa defines a read-only link from the Store Module's `StoreLocale` data model to the Translation Module's `Locale` data model. This means you can retrieve the details of a store's supported locales, but you don't manage the links in a pivot table in the database.

### Retrieve with Query

To retrieve the details of a store's locales with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_locales.locale.*` in `fields`:

### query.graph

```ts
const { data: stores } = await query.graph({
  entity: "store",
  fields: [
    "supported_locales.locale.*",
  ],
})

// stores[0].supported_locales[0].locale
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: stores } = useQueryGraphStep({
  entity: "store",
  fields: [
    "supported_locales.locale.*",
  ],
})

// stores[0].supported_locales[0].locale
```


# Translation Module

In this section of the documentation, you will find resources to learn more about the Translation Module and how to use it in your application.

### Prerequisites

- [Medusa v2.12.3 or later](https://github.com/medusajs/medusa/releases/tag/v2.12.3)
- [Translation Feature Flag Enabled](#configure-translation-module)

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/translations/index.html.md) to learn how to manage translations in the dashboard.

Medusa has translation features available out-of-the-box through the Translation Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features is provided in Commerce Modules, such as the Translation Module.

Refer to the [Module Isolation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md) guide to learn more about why modules are isolated.

## Translation Features

- [Translation and Locale Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/translation/concepts/index.html.md): Manage locales and add translations for different resources in your store.
- [Multi-Language Support](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/translation/storefront/index.html.md): Manage and serve resources like products in multiple languages to cater to a diverse customer base.
- [Translation for Custom Models](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/translation/custom-data-models/index.html.md): Extend translation capabilities to custom data models in your Medusa application.

***

## Configure Translation Module

The Translation Module is currently behind a feature flag. To use it in your Medusa application, add it to the `modules` array and enable the `translation` feature flag.

In your `medusa-config.ts` file, add the Translation Module to the `modules` array and enable the `translation` feature flag:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    // other modules...
    {
      resolve: "@medusajs/medusa/translation",
    },
  ],
  featureFlags: {
    translation: true,
  },
})
```

Then, run the following command to make the necessary database changes for the Translation Module:

```bash
npx medusa db:migrate
```

You can then use the Translation Module in your Medusa application.

***

## How to Use the Translation Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and a reliable rollback mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-translation.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createTranslationStep = createStep(
  "create-translation",
  async ({}, { container }) => {
    const translationModuleService = container.resolve(Modules.TRANSLATION)

    const translation = await translationModuleService.createTranslations({
      reference_id: "product_123",
      reference: "product",
      locale_code: "fr-FR",
      translations: {
        title: "Produit Exemple",
        description: "Ceci est une description en français.",
      },
    })

    return new StepResponse({ translation }, translation.id)
  },
  async (translationId, { container }) => {
    const translationModuleService = container.resolve(Modules.TRANSLATION)

    await translationModuleService.deleteTranslations([translationId])
  }
)

export const createTranslationWorkflow = createWorkflow(
  "create-translation",
  () => {
    const { translation } = createTranslationStep()

    return new WorkflowResponse({
      translation,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createTranslationWorkflow } from "../../workflows/create-translation"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createTranslationWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createTranslationWorkflow } from "../workflows/create-translation"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createTranslationWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createTranslationWorkflow } from "../workflows/create-translation"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createTranslationWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) documentation to learn more.

***

## Supported Module Translations

The Translation Module currently supports translations for the following data models:

|Data Model|Translatable Fields|
|---|---|
|\`CustomerGroup\`|\`name\`|
|\`Product\`||
|\`ProductCategory\`||
|\`ProductCollection\`|\`title\`|
|\`ProductOption\`|\`title\`|
|\`ProductOptionValue\`|\`value\`|
|\`ProductTag\`|\`value\`|
|\`ProductType\`|\`value\`|
|\`ProductVariant\`||
|\`Region\`|\`name\`|
|\`ShippingOption\`|\`name\`|
|\`ShippingOptionType\`||
|\`TaxRate\`|\`name\`|

Future versions of the Translation Module will include support for all Commerce Modules.

***


# Serve Translations in the Storefront

In this guide, you’ll learn how to use locales and serve translated content in your storefront application in your Medusa application.

### Prerequisites

- [Translation Module Configured](https://docs.medusajs.com/commerce-modules/translation#configure-translation-module/index.html.md)

## Fetch Store Locales

You can retrieve the list of locales available in your store using the [List Locales Store API route](https://docs.medusajs.com/api/store#locales_getlocales). You can display this list to customers, allowing them to select their preferred language.

For example, to fetch the list of locales, send a `GET` request to the following endpoint:

```bash
curl "http://localhost:9000/store/locales" \
-H 'x-publishable-api-key: {your_publishable_api_key}'
```

Learn more in the [Localization](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/localization/index.html.md) storefront guide.

You must pass a publishable API key in the request header to store API routes. Learn more in the [Store API reference](https://docs.medusajs.com/api/store#publishable-api-key).

***

## Retrieve Translations for Resources

You can retrieve translations using the [Store API routes](https://docs.medusajs.com/api/store) for [supported](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/translation#supported-module-translations/index.html.md) and [custom](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/translation/custom-data-models/index.html.md) data models. Future releases will expand translation support to additional resources.

Medusa determines the locale for the request in the following order of priority:

- The `locale` query parameter in the API request.
- The `x-medusa-locale` header in the API request.

If translations aren't available for the selected locale, or no locale is selected, the original content stored in the resource's data model is returned.

For example, to retrieve products with translations in French (France):

```bash
curl "http://localhost:9000/store/products?locale=fr-FR" \
-H 'x-publishable-api-key: {your_publishable_api_key}'
```

This returns the list of products with their French (France) translations if available:

```json
{
  "products": [
    {
      "id": "prod_123",
      "title": "Produit Exemple",
      "description": "Ceci est une description en français.",
      // other product fields...
    },
    // other products...
  ]
}
```

Learn more in the [List Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/products/list#retrieve-translations-for-products/index.html.md) storefront guide.

***

## Set a Cart's Locale

A cart can have a locale, which determines the language of the item contents. You can set the locale when creating the cart and update it if the user changes their language preference.

For example, when creating a cart, set the `locale` property in the request body:

```bash
curl -X POST "http://localhost:9000/store/carts" \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
-H "Content-Type: application/json" \
-d '{
  // other cart properties...
  "locale": "fr-FR"
}'
```

This creates a cart with the French (France) locale. When you add items to the cart, their titles and descriptions are displayed in French if translations are available.

If you don't specify a locale when creating the cart, the original product content is used for the items in the cart.

Learn more in the [Create Cart](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/cart/create/index.html.md) and [Update Cart](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/cart/update/index.html.md) storefront guides.

***

## Locale of Placed Order

When a cart is completed and an order is placed, the locale of the cart is copied to the order. The content of items in the order is displayed in the locale that was set in the cart.

This ensures that customers see order details in their preferred language.


# Send Invite User Email Notification

In this guide, you'll learn how to handle the `invite.created` and `invite.resent` events to send an invite email (or other notification type) to the user.

Refer to the [Manage Invites](https://docs.medusajs.com/user-guide/settings/users/invites/index.html.md) user guide to learn how to manage invites using the Medusa Admin.

## User Invite Flow Overview

![Diagram showcasing the user invite flow detailed below](https://res.cloudinary.com/dza7lstvk/image/upload/v1754047213/Medusa%20Resources/invite-user_uqspsv.jpg)

Admin users can add new users to their store by sending them an invite. The flow for inviting users is as follows:

1. An admin user invites a user either through the [Medusa Admin](https://docs.medusajs.com/user-guide/settings/users/invites/index.html.md) or using the [Create Invite API route](https://docs.medusajs.com/api/admin#invites_postinvites).
2. The invite is created and the `invite.created` event is emitted.
   - At this point, you can handle the event to send an email or notification to the user.
3. The invited user receives the invite and can accept it, which creates a new user.
   - The invited user can accept the invite either through the Medusa Admin or using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept).

The admin user can also resend the invite if the invited user doesn't receive the invite or doesn't accept it before expiry, which emits the `invite.resent` event.

In this guide, you'll implement a subscriber that handles the `invite.created` and `invite.resent` events to send an email to the user.

After adding the subscriber, you will have a complete user invite flow that you can utilize either through the Medusa Admin or using the Admin APIs.

***

## Prerequisites: Notification Module Provider

To send an email or notification to the user, you must have a Notification Module Provider set up.

Medusa provides providers like [SendGrid](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md) and [Resend](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/integrations/guides/resend/index.html.md), and you can also [create your own custom provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md).

Refer to the [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification#what-is-a-notification-module-provider/index.html.md) documentation for a list of available providers and how to set them up.

### Testing with the Local Notification Module Provider

For testing purposes, you can use the [Local Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/local/index.html.md) by adding this to your `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          // ...
          {
            resolve: "@medusajs/medusa/notification-local",
            id: "local",
            options: {
              channels: ["email"],
            },
          },
        ],
      },
    },
  ],
})
```

The Local provider logs email details to your terminal instead of sending actual emails, which is useful for development and testing.

***

## Create the Invite User Subscriber

To create a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that handles the `invite.created` and `invite.resent` events, create the file `src/subscribers/user-invited.ts` with the following content:

```ts title="src/subscribers/user-invited.ts" highlights={highlights}
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"

export default async function inviteCreatedHandler({
  event: { data },
  container,
}: SubscriberArgs<{
  id: string
}>) {
  const query = container.resolve("query")
  const notificationModuleService = container.resolve(
    "notification"
  )
  const config = container.resolve("configModule")

  const { data: [invite] } = await query.graph({
    entity: "invite",
    fields: [
      "email",
      "token",
    ],
    filters: {
      id: data.id,
    },
  })

  const backend_url = config.admin.backendUrl !== "/" ? config.admin.backendUrl :
    "http://localhost:9000"
  const adminPath = config.admin.path

  await notificationModuleService.createNotifications({
    to: invite.email,
    // TODO replace with template ID in notification provider
    template: "user-invited",
    channel: "email",
    data: {
      invite_url: `${backend_url}${adminPath}/invite?token=${invite.token}`,
    },
  })
}

export const config: SubscriberConfig = {
  event: [
    "invite.created",
    "invite.resent",
  ],
}
```

The subscriber receives the ID of the invite in the event payload. You resolve [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to fetch the invite details, including the email and token.

### Invite URL

You then use the Notification Module's service to send an email to the user with the invite link. The invite link is constructed using:

1. The backend URL of the Medusa application, since the Medusa Admin is hosted on the same URL.
2. The admin path, which is the path to the Medusa Admin (for example, `/app`).

Note that the Medusa Admin has an invite form at `/invite?token=`, which accepts the invite token as a query parameter.

### Notification Configurations

For the notification, you can configure the following fields:

- `template`: The template ID of the email to send. This ID depends on the Notification Module provider you use. For example, if you use SendGrid, this would be the ID of the SendGrid template.
  - Refer to the [Example Notification Templates](#example-notification-templates) section below for examples of notification templates to use.
- `channel`: The channel to send the notification through. In this case, it's set to `email`.
- `data`: The data to pass to the notification template. You can add additional fields as needed, such as the invited user's email.

### Test It Out

After you set up the Notification Module Provider, create a template in the provider, and create the subscriber, you can test the full invite flow.

Start the Medusa application with the following command:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin (locally at `http://localhost:9000/app`) and go to Settings → Users. Create an invite as explained in the [Manage Invites](https://docs.medusajs.com/user-guide/settings/users/invites/index.html.md) user guide.

Once you create the invite, you should see that the `invite.created` event is emitted in the server's logs:

```bash
info:    Processing invite.created which has 1 subscribers
```

If you're using an email Notification Module Provider, check the recipient's email inbox for the invite email with the link to accept the invite.

If you're using the Local provider, check your terminal for the logged email details.

***

## Example Notification Templates

The following section provides example notification templates for some Notification Module Providers.

### SendGrid

Refer to the [SendGrid Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md) documentation for more details on how to set up SendGrid.

The following HTML template can be used with SendGrid to send an invite email:

```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>You're Invited!</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
            background-color: #ffffff;
            margin: 0;
            padding: 20px;
        }
        .container {
            max-width: 465px;
            margin: 40px auto;
            border: 1px solid #eaeaea;
            border-radius: 5px;
            padding: 20px;
        }
        .header {
            text-align: center;
            margin: 30px 0;
        }
        .title {
            color: #000000;
            font-size: 24px;
            font-weight: normal;
            margin: 0;
        }
        .content {
            margin: 32px 0;
        }
        .text {
            color: #000000;
            font-size: 14px;
            line-height: 24px;
            margin: 0 0 16px 0;
        }
        .button-container {
            text-align: center;
            margin: 32px 0;
        }
        .invite-button {
            background-color: #000000;
            border-radius: 3px;
            color: #ffffff;
            font-size: 12px;
            font-weight: 600;
            text-decoration: none;
            text-align: center;
            padding: 12px 20px;
            display: inline-block;
        }
        .invite-button:hover {
            background-color: #333333;
        }
        .url-section {
            margin: 32px 0;
        }
        .url-link {
            color: #2563eb;
            text-decoration: none;
            font-size: 14px;
            line-height: 24px;
            word-break: break-all;
        }
        .footer {
            margin-top: 32px;
        }
        .footer-text {
            color: #666666;
            font-size: 12px;
            line-height: 24px;
            margin: 0;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1 class="title">You're Invited!</h1>
        </div>

        <div class="content">
            <p class="text">
                Hello{{#if email}} {{email}}{{/if}},
            </p>
            <p class="text">
                You've been invited to join our platform. Click the button below to accept your invitation and set up your account.
            </p>
        </div>

        <div class="button-container">
            <a href="{{invite_url}}" class="invite-button">
                Accept Invitation
            </a>
        </div>

        <div class="url-section">
            <p class="text">
                Or copy and paste this URL into your browser:
            </p>
            <a href="{{invite_url}}" class="url-link">
                {{invite_url}}
            </a>
        </div>

        <div class="footer">
            <p class="footer-text">
                If you weren't expecting this invitation, you can ignore this email.
            </p>
        </div>
    </div>
</body>
</html>
```

Make sure to pass the `invite_url` variable to the template, which contains the URL to accept the invite.

You can also customize the template further to show other information, such as the user's email.

### Resend

If you've integrated Resend as explained in the [Resend Integration Guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/integrations/guides/resend/index.html.md), you can add a new template for user invites at `src/modules/resend/emails/user-invited.tsx`:

```tsx title="src/modules/resend/emails/user-invited.tsx"
import { 
  Text, 
  Container, 
  Heading, 
  Html, 
  Section, 
  Tailwind, 
  Head, 
  Preview, 
  Body, 
  Link,
  Button, 
} from "@react-email/components"

type UserInvitedEmailProps = {
  invite_url: string
  email?: string
}

function UserInvitedEmailComponent({ invite_url, email }: UserInvitedEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>You've been invited to join our platform</Preview>
      <Tailwind>
        <Body className="bg-white my-auto mx-auto font-sans px-2">
          <Container className="border border-solid border-[#eaeaea] rounded my-[40px] mx-auto p-[20px] max-w-[465px]">
            <Section className="mt-[32px]">
              <Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
                You're Invited!
              </Heading>
            </Section>

            <Section className="my-[32px]">
              <Text className="text-black text-[14px] leading-[24px]">
                Hello{email ? ` ${email}` : ""},
              </Text>
              <Text className="text-black text-[14px] leading-[24px]">
                You've been invited to join our platform. Click the button below to accept your invitation and set up your account.
              </Text>
            </Section>

            <Section className="text-center mt-[32px] mb-[32px]">
              <Button
                className="bg-[#000000] rounded text-white text-[12px] font-semibold no-underline text-center px-5 py-3"
                href={invite_url}
              >
                Accept Invitation
              </Button>
            </Section>

            <Section className="my-[32px]">
              <Text className="text-black text-[14px] leading-[24px]">
                Or copy and paste this URL into your browser:
              </Text>
              <Link
                href={invite_url}
                className="text-blue-600 no-underline text-[14px] leading-[24px] break-all"
              >
                {invite_url}
              </Link>
            </Section>

            <Section className="mt-[32px]">
              <Text className="text-[#666666] text-[12px] leading-[24px]">
                If you weren't expecting this invitation, you can ignore this email.
              </Text>
            </Section>
          </Container>
        </Body>
      </Tailwind>
    </Html>
  )
}

export const userInvitedEmail = (props: UserInvitedEmailProps) => (
  <UserInvitedEmailComponent {...props} />
)

// Mock data for preview/development
const mockInvite: UserInvitedEmailProps = {
  invite_url: "https://your-app.com/app/invite/sample-token-123",
  email: "user@example.com",
}

export default () => <UserInvitedEmailComponent {...mockInvite} />
```

Feel free to customize the email template further to match your branding and style, or to add additional information such as the user's email.

Then, in the Resend Module's service at `src/modules/resend/service.ts`, add the new template to the `templates` object and `Templates` type:

```ts title="src/modules/resend/service.ts"
// other imports...
import { userInvitedEmail } from "./emails/user-invited"

enum Templates {
  // ...
  USER_INVITED = "user-invited",
}

const templates: {[key in Templates]?: (props: unknown) => React.ReactNode} = {
  // ...
  [Templates.USER_INVITED]: userInvitedEmail,
}
```

Finally, find the `getTemplateSubject` function in the `ResendNotificationProviderService` and add a case for the `USER_INVITED` template:

```ts title="src/modules/resend/service.ts"
class ResendNotificationProviderService extends AbstractNotificationProviderService {
  // ...

  private getTemplateSubject(template: Templates) {
    // ...
    switch (template) {
      // ...
      case Templates.USER_INVITED:
        return "You've been invited to join our platform"
    }
  }
}
```


# User Module Options

In this guide, you'll learn about the options you can pass to the User Module.

## Options Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/user",
      options: {
        jwt_secret: process.env.JWT_SECRET,
        jwt_public_key: process.env.JWT_PUBLIC_KEY,
        valid_duration: 60 * 60 * 24, // 24 hours
        jwt_options: {
          algorithm: process.env.JWT_ALGORITHM || "RS256",
          issuer: process.env.JWT_ISSUER || "medusa",
        },
        jwt_verify_options: {
          algorithms: [process.env.JWT_ALGORITHM || "RS256"],
          issuer: process.env.JWT_ISSUER || "medusa",
        },
      },
    },
  ],
})
```

### Environment Variables

Make sure to add the necessary environment variables for the above options to your `.env` file:

```bash
JWT_SECRET=supersecret
# Optional: For asymmetric key validation
JWT_PUBLIC_KEY=your_public_key_here
JWT_ALGORITHM=RS256
JWT_ISSUER=medusa
```

### All Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`jwt\_secret\`|A string indicating the secret used to sign the invite tokens.|Yes|-|
|\`jwt\_public\_key\`|A string indicating the public key used to verify JWT tokens when using asymmetric validation. Only used when the JWT secret is a private key for asymmetric signing.|No|-|
|\`valid\_duration\`|A number indicating the duration in seconds that an invite token is valid. This is used to set the expiration time for invite tokens.|No|\`86400\`|
|\`jwt\_options\`|An object containing options for signing JWT tokens when using asymmetric signing with a private/public key pair. Accepts any options from |No|\`\{}\`|
|\`jwt\_verify\_options\`|An object containing options for verifying JWT tokens when using asymmetric validation with a private/public key pair. Accepts any options from |No|Value of |


# User Module

In this section of the documentation, you will find resources to learn more about the User Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/users/index.html.md) to learn how to manage users using the dashboard.

Medusa has user related features available out-of-the-box through the User Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this User Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## User Features

- [User Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows/index.html.md): Store and manage users in your store.
- [Invite Users](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows#invite-users/index.html.md): Invite users to join your store and manage those invites.

***

## How to Use User Module's Service

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-user.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createUserStep = createStep(
  "create-user",
  async ({}, { container }) => {
    const userModuleService = container.resolve(Modules.USER)

    const user = await userModuleService.createUsers({
      email: "user@example.com",
      first_name: "John",
      last_name: "Smith",
    })

    return new StepResponse({ user }, user.id)
  },
  async (userId, { container }) => {
    if (!userId) {
      return
    }
    const userModuleService = container.resolve(Modules.USER)

    await userModuleService.deleteUsers([userId])
  }
)

export const createUserWorkflow = createWorkflow(
  "create-user",
  () => {
    const { user } = createUserStep()

    return new WorkflowResponse({
      user,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createUserWorkflow } from "../../workflows/create-user"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createUserWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createUserWorkflow } from "../workflows/create-user"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createUserWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createUserWorkflow } from "../workflows/create-user"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createUserWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***

## Configure User Module

The User Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/module-options/index.html.md) for details on the module's options.

***


# User Creation Flows

In this document, learn the different ways to create a user using the User Module.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/users/index.html.md) to learn how to manage users using the dashboard.

## Straightforward User Creation

To create a user, use the [createUsers method of the User Module’s main service](https://docs.medusajs.com/references/user/createUsers/index.html.md):

```ts
const user = await userModuleService.createUsers({
  email: "user@example.com",
})
```

You can pair this with the Auth Module to allow the user to authenticate, as explained in a [later section](#create-identity-with-the-auth-module).

***

## Invite Users

To create a user, you can create an invite for them using the [createInvites method](https://docs.medusajs.com/references/user/createInvites/index.html.md) of the User Module's main service:

```ts
const invite = await userModuleService.createInvites({
  email: "user@example.com",
})
```

Later, you can accept the invite and create a new user for them:

```ts
const invite =
  await userModuleService.validateInviteToken("secret_123")

await userModuleService.updateInvites({
  id: invite.id,
  accepted: true,
})

const user = await userModuleService.createUsers({
  email: invite.email,
})
```

### Invite Expiry

An invite has an expiry date. You can renew the expiry date and refresh the token using the [refreshInviteTokens method](https://docs.medusajs.com/references/user/refreshInviteTokens/index.html.md):

```ts
await userModuleService.refreshInviteTokens(["invite_123"])
```

***

## Create Identity with the Auth Module

By combining the User and Auth Modules, you can use the Auth Module for authenticating users, and the User Module to manage those users.

So, when a user is authenticated, and you receive the `AuthIdentity` object, you can use it to create a user if it doesn’t exist:

```ts
const { success, authIdentity } =
  await authModuleService.authenticate("emailpass", {
    // ...
  })

const [, count] = await userModuleService.listAndCountUsers({
  email: authIdentity.entity_id,
})

if (!count) {
  const user = await userModuleService.createUsers({
    email: authIdentity.entity_id,
  })
}
```


# Local Analytics Module Provider

The Local Analytics Module Provider is a simple analytics provider for Medusa that logs analytics events to the console. It's useful for development and debugging purposes.

The Analytics Module and its providers are available starting [Medusa v2.8.3](https://github.com/medusajs/medusa/releases/tag/v2.8.3).

***

## Register the Local Analytics Module

Add the module into the `provider` object of the Analytics Module:

You can use only one Analytics Module Provider in your Medusa application.

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/analytics",
      options: {
        providers: [
          {
            resolve: "@medusajs/analytics-local",
            id: "local",
          },
        ],
      },
    },
  ],
})
```

### Change Log Level

The Local Analytics Module Provider logs events to the console at the `debug` level, which is below the default `http` level.

So, to see the tracked events in your logs, you need to change the log level to `debug` or `silly`. You can do so by setting the `LOG_LEVEL` system environment variable:

```bash
export LOG_LEVEL=debug
```

The environment variable must be set as a system environment variable and not in `.env`.

***

## Test out the Module

To test the module out, you'll track in the console when an order is placed.

You'll first create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) that tracks the order completion event. Then, you can execute the workflow in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that listens to the `order.placed` event.

For example, create a workflow at `src/workflows/track-order-placed.ts` with the following content:

```ts title="src/workflows/track-order-placed.ts" highlights={workflowHighlights}
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { Modules } from "@medusajs/framework/utils"
import { OrderDTO } from "@medusajs/framework/types"

type StepInput = {
  order: OrderDTO
}

const trackOrderPlacedStep = createStep(
  "track-order-placed-step",
  async ({ order }: StepInput, { container }) => {
    const analyticsModuleService = container.resolve(Modules.ANALYTICS)

    await analyticsModuleService.track({
      event: "order_placed",
      actor_id: order.customer_id,
      properties: {
        order_id: order.id,
        total: order.total,
        items: order.items?.map((item) => ({
          variant_id: item.variant_id,
          product_id: item.product_id,
          quantity: item.quantity,
        })),
        customer_id: order.customer_id,
      },
    })
  }
)

type WorkflowInput = {
  order_id: string
}

export const trackOrderPlacedWorkflow = createWorkflow(
  "track-order-placed-workflow",
  ({ order_id }: WorkflowInput) => {
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "*",
        "customer.*",
        "items.*",
      ],
      filters: {
        id: order_id,
      },
    })
    trackOrderCreatedStep({
      order: orders[0],
    } as unknown as StepInput)
  }
)
```

This workflow retrieves the order details using the `useQueryGraphStep` and then tracks the order placement event using the `trackOrderPlacedStep`.

In the step, you resolve the service of the Analytics Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) and use its `track` method to track the event. This method will use the underlying provider configured (which is the Local Analytics Module Provider, in this case) to track the event.

Next, create a subscriber at `src/subscribers/order-placed.ts` with the following content:

```ts title="src/subscribers/order-placed.ts"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { trackOrderPlacedWorkflow } from "../workflows/track-order-placed"

export default async function orderPlacedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await trackOrderPlacedWorkflow(container).run({
    input: {
      order_id: data.id,
    },
  })
}

export const config: SubscriberConfig = {
  event: "order.placed",
}
```

This subscriber listens to the `order.placed` event and executes the `trackOrderPlacedWorkflow` workflow, passing the order ID as input.

You'll now track the order placement event whenever an order is placed in your Medusa application. You can test this out by placing an order and checking the server's logs for the tracked event.

***

## Additional Resources

- [How to Use the Analytics Module](https://docs.medusajs.com/references/analytics/service/index.html.md)


# Analytics Module

In this document, you'll learn about the Analytics Module and its providers.

The Analytics Module is available starting [Medusa v2.8.3](https://github.com/medusajs/medusa/releases/tag/v2.8.3).

## What is the Analytics Module?

The Analytics Module exposes functionalities to track and analyze user interactions and system events with third-party services. For example, you can track cart updates or completed orders.

In your Medusa application, you can use the Analytics Module to send data to third-party analytics services like PostHog or Segment, enabling you to gain insights into user behavior and system performance.

![Analytics Module workflow diagram showing the event tracking flow: when a customer places an order in the storefront, the Medusa application uses the Analytics Module to capture the event data and forwards it to configured third-party analytics services for business intelligence and user behavior analysis](https://res.cloudinary.com/dza7lstvk/image/upload/v1747832107/Medusa%20Resources/analytics-module-overview_egz7xg.jpg)

***

## How to Use the Analytics Module?

### Configure Analytics Module Provider

To use the Analytics Module, you need to configure it along with an Analytics Module Provider.

An Analytics Module Provider implements the underlying logic of sending analytics data. It integrates with a third-party analytics service to send the data collected through the Analytics Module.

Medusa provides two Analytics Module Providers: Local and PostHog module providers.

You can also [create a custom Analytics Module Provider](https://docs.medusajs.com/references/analytics/provider/index.html.md) that integrates with a third-party service, like Segment.

- [Local](https://docs.medusajs.com/infrastructure-modules/analytics/local/index.html.md)
- [PostHog](https://docs.medusajs.com/infrastructure-modules/analytics/posthog/index.html.md)

[Segment](https://docs.medusajs.com/integrations/guides/segment/index.html.md): undefined

To configure the Analytics Module and its provider, add it to the list of modules in your `medusa-config.ts` file. For example:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/analytics",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/analytics-local",
            id: "local",
          },
        ],
      },
    },
  ],
})
```

Refer to the documentation of each provider for specific configuration options.

### Track Events

To track an event, you can use the Analytics Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

In a step of your workflow, you can resolve the Analytics Module's service and use its methods to track events or identify users.

For example, create a workflow at `src/workflows/track-order-placed.ts` with the following content:

```ts title="src/workflows/track-order-placed.ts" highlights={workflowHighlights}
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { Modules } from "@medusajs/framework/utils"
import { OrderDTO } from "@medusajs/framework/types"

type StepInput = {
  order: OrderDTO
}

const trackOrderPlacedStep = createStep(
  "track-order-placed-step",
  async ({ order }: StepInput, { container }) => {
    const analyticsModuleService = container.resolve(Modules.ANALYTICS)

    await analyticsModuleService.track({
      event: "order_placed",
      actor_id: order.customer_id,
      properties: {
        order_id: order.id,
        total: order.total,
        items: order.items?.map((item) => ({
          variant_id: item.variant_id,
          product_id: item.product_id,
          quantity: item.quantity,
        })),
        customer_id: order.customer_id,
      },
    })
  }
)

type WorkflowInput = {
  order_id: string
}

export const trackOrderPlacedWorkflow = createWorkflow(
  "track-order-placed-workflow",
  ({ order_id }: WorkflowInput) => {
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "*",
        "customer.*",
        "items.*",
      ],
      filters: {
        id: order_id,
      },
    })
    trackOrderCreatedStep({
      order: orders[0],
    } as unknown as StepInput)
  }
)
```

This workflow retrieves the order details using the `useQueryGraphStep` and then tracks the order placement event using the `trackOrderPlacedStep`.

In the step, you resolve the service of the Analytics Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) and use its `track` method to track the event. This method will use the underlying provider configured in `medusa-config.ts` to track the event.

### Execute Analytics Workflow

After that, you can execute this workflow in a subscriber that runs when a product is created.

create a subscriber at `src/subscribers/order-placed.ts` with the following content:

```ts title="src/subscribers/order-placed.ts"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { trackOrderPlacedWorkflow } from "../workflows/track-order-placed"

export default async function orderPlacedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await trackOrderPlacedWorkflow(container).run({
    input: {
      order_id: data.id,
    },
  })
}

export const config: SubscriberConfig = {
  event: "order.placed",
}
```

This subscriber listens to the `order.placed` event and executes the `trackOrderPlacedWorkflow` workflow, passing the order ID as input.

You'll now track the order placement event whenever an order is placed in your Medusa application. You can test this out by placing an order and checking the provider you integrated with (for example, PostHog) for the tracked event.


# PostHog Analytics Module Provider

The PostHog Analytics Module Provider allows you to integrate [PostHog](https://posthog.com/) with Medusa.

PostHog is an open-source product analytics platform that helps you track user interactions and analyze user behavior in your commerce application.

By integrating PostHog with Medusa, you can track events such as cart additions, order completions, and user sign-ups, enabling you to gain insights into user behavior and optimize your application accordingly.

The Analytics Module and its providers are available starting [Medusa v2.8.3](https://github.com/medusajs/medusa/releases/tag/v2.8.3).

***

## Register the PostHog Analytics Module

### Prerequisites

- [PostHog account](https://app.posthog.com/signup)
- [PostHog API Key](https://posthog.com/docs/api)

Add the module into the `provider` object of the Analytics Module:

You can use only one provider in your Medusa application.

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/analytics",
      options: {
        providers: [
          {
            resolve: "@medusajs/analytics-posthog",
            id: "posthog",
            options: {
              posthogEventsKey: process.env.POSTHOG_EVENTS_API_KEY,
              posthogHost: process.env.POSTHOG_HOST,
            },
          },
        ],
      },
    },
  ],
})
```

### Environment Variables

Make sure to add the following environment variables:

```bash
POSTHOG_EVENTS_API_KEY=<YOUR_POSTHOG_EVENTS_API_KEY>
POSTHOG_HOST=<YOUR_POSTHOG_HOST>
```

### PostHog Analytics Module Options

|Option|Description|Default|
|---|---|---|
|\`eventsKey\`|The PostHog API key for tracking events. This is required to authenticate your requests to the PostHog API.|-|
|\`posthogHost\`|The PostHog API host URL.|\`https://eu.i.posthog.com\`|

***

## Test out the Module

To test the module out, you'll track in PostHog when an order is placed.

You'll first create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) that tracks the order completion event. Then, you can execute the workflow in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that listens to the `order.placed` event.

For example, create a workflow at `src/workflows/track-order-placed.ts` with the following content:

```ts title="src/workflows/track-order-placed.ts" highlights={workflowHighlights}
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { Modules } from "@medusajs/framework/utils"
import { OrderDTO } from "@medusajs/framework/types"

type StepInput = {
  order: OrderDTO
}

const trackOrderPlacedStep = createStep(
  "track-order-placed-step",
  async ({ order }: StepInput, { container }) => {
    const analyticsModuleService = container.resolve(Modules.ANALYTICS)

    await analyticsModuleService.track({
      event: "order_placed",
      actor_id: order.customer_id,
      properties: {
        order_id: order.id,
        total: order.total,
        items: order.items?.map((item) => ({
          variant_id: item.variant_id,
          product_id: item.product_id,
          quantity: item.quantity,
        })),
        customer_id: order.customer_id,
      },
    })
  }
)

type WorkflowInput = {
  order_id: string
}

export const trackOrderPlacedWorkflow = createWorkflow(
  "track-order-placed-workflow",
  ({ order_id }: WorkflowInput) => {
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "*",
        "customer.*",
        "items.*",
      ],
      filters: {
        id: order_id,
      },
    })
    trackOrderCreatedStep({
      order: orders[0],
    } as unknown as StepInput)
  }
)
```

This workflow retrieves the order details using the `useQueryGraphStep` and then tracks the order placement event using the `trackOrderPlacedStep`.

In the step, you resolve the service of the Analytics Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) and use its `track` method to track the event. This method will use the underlying provider configured (which is the PostHog Analytics Module Provider, in this case) to track the event.

Next, create a subscriber at `src/subscribers/order-placed.ts` with the following content:

```ts title="src/subscribers/order-placed.ts"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { trackOrderPlacedWorkflow } from "../workflows/track-order-placed"

export default async function orderPlacedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await trackOrderPlacedWorkflow(container).run({
    input: {
      order_id: data.id,
    },
  })
}

export const config: SubscriberConfig = {
  event: "order.placed",
}
```

This subscriber listens to the `order.placed` event and executes the `trackOrderPlacedWorkflow` workflow, passing the order ID as input.

You'll now track the order placement event whenever an order is placed in your Medusa application. You can test this out by placing an order and checking your PostHog dashboard for the tracked event.

***

## Additional Resources

- [How to Use the Analytics Module](https://docs.medusajs.com/references/analytics/service/index.html.md)


# How to Create a Cache Module

In this guide, you’ll learn how to create a Cache Module.

{/* TODO add link */}

The Cache Module is deprecated starting from [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0). [Create a Caching Module Provider](#) instead.

## 1. Create Module Directory

Start by creating a new directory for your module. For example, `src/modules/my-cache`.

***

## 2. Create the Cache Service

Create the file `src/modules/my-cache/service.ts` that holds the implementation of the cache service.

The Cache Module's main service must implement the `ICacheService` interface imported from `@medusajs/framework/types`:

```ts title="src/modules/my-cache/service.ts"
import { ICacheService } from "@medusajs/framework/types"

class MyCacheService implements ICacheService {
  get<T>(key: string): Promise<T> {
    throw new Error("Method not implemented.")
  }
  set(key: string, data: unknown, ttl?: number): Promise<void> {
    throw new Error("Method not implemented.")
  }
  invalidate(key: string): Promise<void> {
    throw new Error("Method not implemented.")
  }
}

export default MyCacheService
```

The service implements the required methods based on the desired caching mechanism.

### Implement get Method

The `get` method retrieves the value of a cached item based on its key.

The method accepts a string as a first parameter, which is the key in the cache. It either returns the cached item or `null` if it doesn’t exist.

For example, to implement this method using Memcached:

```ts title="src/modules/my-cache/service.ts"
class MyCacheService implements ICacheService {
  // ...
  async get<T>(cacheKey: string): Promise<T | null> {
    return new Promise((res, rej) => {
      this.memcached.get(cacheKey, (err, data) => {
        if (err) {
          res(null)
        } else {
          if (data) {
            res(JSON.parse(data))
          } else {
            res(null)
          }
        }
      })
    })
  }
}
```

### Implement set Method

The `set` method is used to set an item in the cache. It accepts three parameters:

1. The first parameter is a string indicating the key of the data being added to the cache. This key can be used later to get or invalidate the cached item.
2. The second parameter is the data to be added to the cache. The data can be of any type.
3. The third parameter is optional. It’s a number indicating how long (in seconds) the data should be kept in the cache.

For example, to implement this method using Memcached:

```ts title="src/modules/my-cache/service.ts"
class MyCacheService implements ICacheService {
  protected TTL = 60
  // ...
  async set(
    key: string,
    data: Record<string, unknown>,
    ttl: number = this.TTL // or any value
  ): Promise<void> {
    return new Promise((res, rej) =>
      this.memcached.set(
        key, JSON.stringify(data), ttl, (err) => {
        if (err) {
          rej(err)
        } else {
          res()
        }
      })
    )
  }
}
```

### Implement invalidate Method

The `invalidate` method removes an item from the cache using its key.

By default, items are removed from the cache when their time-to-live (ttl) expires. The `invalidate` method can be used to remove the item beforehand.

The method accepts a string as a first parameter, which is the key of the item to invalidate and remove from the cache.

For example, to implement this method using Memcached:

```ts title="src/modules/my-cache/service.ts"
class MyCacheService implements ICacheService {
  // ...
  async invalidate(key: string): Promise<void> {
    return new Promise((res, rej) => {
      this.memcached.del(key, (err) => {
        if (err) {
          rej(err)
        } else {
          res()
        }
      })
    })
  }
}
```

***

## 3. Create Module Definition File

Create the file `src/modules/my-cache/index.ts` with the following content:

```ts title="src/modules/my-cache/index.ts"
import MyCacheService from "./service"
import { Module } from "@medusajs/framework/utils"

export default Module("my-cache", {
  service: MyCacheService,
})
```

This exports the module's definition, indicating that the `MyCacheService` is the main service of the module.

***

## 4. Use Module

To use your Cache Module, add it to the `modules` object exported as part of the configurations in `medusa-config.ts`. A Cache Module is added under the `cacheService` key.

For example:

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/my-cache",
      options: {
        // any options
        ttl: 30,
      },
    },
  ],
})
```


# In-Memory Cache Module

The In-Memory Cache Module uses a plain JavaScript Map object to store the cached data. This module is used by default in your Medusa application.

This module is helpful for development or when you’re testing out Medusa, but it’s not recommended to be used in production.

For production, it’s recommended to use modules like [Redis Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/redis/index.html.md).

The In-Memory Cache Module is deprecated starting from [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0). Use the [Caching Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching/index.html.md) instead.

***

## Register the In-Memory Cache Module

The In-Memory Cache Module is registered by default in your application.

Add the module into the `modules` property of the exported object in `medusa-config.ts`:

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"
// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/cache-inmemory",
      options: {
        // optional options
      },
    },
  ],
})
```

### In-Memory Cache Module Options

|Option|Description|Default|
|---|---|---|---|---|
|\`ttl\`|The number of seconds an item can live in the cache before it’s removed.|\`30\`|


# Cache Module

In this document, you'll learn what a Cache Module is and how to use it in your Medusa application.

The Cache Module is deprecated starting from [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0). Use the [Caching Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching/index.html.md) instead.

## What is a Cache Module?

A Cache Module is used to cache the results of computations such as price selection or various tax calculations.

The underlying database, third-party service, or caching logic is flexible since it's implemented in a module. You can choose from Medusa’s cache modules or create your own to support something more suitable for your architecture.

### Default Cache Module

By default, Medusa uses the [In-Memory Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/in-memory/index.html.md). This module uses a plain JavaScript Map object to store the cache data. While this is suitable for development, it's recommended to use other Cache Modules, such as the [Redis Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/redis/index.html.md), for production. You can also [Create a Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/create/index.html.md).

***

## How to Use the Cache Module?

You can use the registered Cache Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

In a step of your workflow, you can resolve the Cache Module's service and use its methods to cache data, retrieve cached data, or clear the cache.

For example:

```ts
import { Modules } from "@medusajs/framework/utils"
import { 
  createStep,
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async ({}, { container }) => {
    const cacheModuleService = container.resolve(
      Modules.CACHE
    )

    await cacheModuleService.set("key", "value")
  } 
)

export const workflow = createWorkflow(
  "workflow-1",
  () => {
    step1()
  }
)
```

In the example above, you create a workflow that has a step. In the step, you resolve the service of the Cache Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

Then, you use the `set` method of the Cache Module to cache the value `"value"` with the key `"key"`.

***

## List of Cache Modules

Medusa provides the following Cache Modules. You can use one of them, or [Create a Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/create/index.html.md).

- [In-Memory](https://docs.medusajs.com/infrastructure-modules/cache/in-memory/index.html.md)
- [Redis](https://docs.medusajs.com/infrastructure-modules/cache/redis/index.html.md)


# Redis Cache Module

The Redis Cache Module uses Redis to cache data in your store. In production, it's recommended to use this module.

The Redis Cache Module is deprecated starting from [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0). Use the [Redis Caching Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching/providers/redis/index.html.md) instead.

***

## Register the Redis Cache Module

### Prerequisites

- [Redis installed and Redis server running](https://redis.io/docs/getting-started/installation/)

Add the module into the `modules` property of the exported object in `medusa-config.ts`:

```ts title="medusa-config.ts" highlights={highlights}
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/cache-redis",
      options: { 
        redisUrl: process.env.CACHE_REDIS_URL,
      },
    },
  ],
})
```

### Environment Variables

Make sure to add the following environment variables:

```bash
CACHE_REDIS_URL=<YOUR_REDIS_URL>
```

### Redis Cache Module Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`redisUrl\`|A string indicating the Redis connection URL.|Yes|-|
|\`redisOptions\`|An object of Redis options. Refer to the |No|-|
|\`ttl\`|The number of seconds an item can live in the cache before it’s removed.|No|\`30\`|
|\`namespace\`|A string used to prefix all cached keys with |No|\`medusa\`|

***

## Test the Module

To test the module, start the Medusa application:

```bash npm2yarn
npm run dev
```

You'll see the following message in the terminal's logs:

```bash noCopy noReport
Connection to Redis in module 'cache-redis' established
```


# Caching Module Concepts

In this guide, you'll learn about the main concepts of the [Caching Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching/index.html.md), including cache keys, cache tags, and automatic cache invalidation.

## Cache Keys

Cache keys uniquely identify cached data in the caching service. The Caching Module automatically generates cache keys when you cache data with Query or the Index Module.

### Custom Cache Keys

When you cache custom data with the Caching Module's service, you can generate a cache key using the [computeKey](https://docs.medusajs.com/references/caching-service#computeKey/index.html.md) method. This method generates a unique key based on the data you want to cache.

For example:

```ts
const data = { id: "prod_123", title: "Product 123" }
const key = await cachingModuleService.computeKey(data)
await cachingModuleService.set({
  key,
  tags: ["Product:prod_123", "Product:list:*"],
  data,
})
```

The `computeKey` method takes an object as input and generates a unique key based on its properties.

The generated key is a hash string that uniquely identifies the data. The has doesn't change based on the order of properties in the object.

For example, the following two objects generate the same cache key:

```ts
const key1 = await cachingModuleService.computeKey({ id: "prod_123", title: "Product 123" })
const key2 = await cachingModuleService.computeKey({ title: "Product 123", id: "prod_123" })

console.log(key1 === key2) // true
```

### When to Use Custom Cache Keys?

Use custom cache keys when you're caching custom data with the Caching Module's service. This ensures that the cached data is uniquely identified and can be retrieved or invalidated correctly.

You can also pass a custom key to Query or the Index Module when caching data. This is useful when you want to use the key for custom invalidation or retrieval.

Learn more about passing custom keys in the [Query guide](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#set-cache-key/index.html.md).

***

## Cache Tags

Cache tags are useful for grouping cached data, making it easier to invalidate or retrieve related cached entries.

When you cache data with the Query or Index Module, the Caching Module automatically generates cache tags based on the entity being queried and its retrieved relations.

When you cache custom data with the Caching Module's service, you can pass custom tags to the `set` and `get` methods.

### Caching Tags Convention

The Caching Module generates cache tags in the following format:

- `Entity:id`: Cache tag for a single record of an entity. For example, `Product:prod_123` for caching a single product with the ID `prod_123`.
- `Entity:list:*`: Cache tag for a list of records of an entity. For example, `Product:list:*` for caching a list of products.

`Entity` is the pascal-cased name of the data model, which you pass as the first parameter to `model.define` when defining the model.

When you use custom tags, ensure they adhere to the above convention. Otherwise, the Caching Module cannot automatically invalidate your cached data. You'll have to [invalidate](https://docs.medusajs.com/references/caching-service#clear/index.html.md) the cached data manually.

For example:

```ts
const key = await cachingModuleService.computeKey(data)
await cachingModuleService.set({
  key,
  tags: ["Product:list:*", "Product:prod_123"],
  data,
})
```

### When to Use Custom Tags?

Use custom tags when you want to group cached data for custom invalidation or retrieval.

Note that if your custom tags do not follow the [Caching Tags Convention](#caching-tags-convention), the Caching Module cannot automatically invalidate your cached data. You must manually [invalidate](https://docs.medusajs.com/references/caching-service#clear/index.html.md) the cached data when it changes.

***

## Automatic Cache Invalidation

### When is Cache Automatically Invalidated?

The Caching Module automatically invalidates cached data when the underlying data changes through database operations such as create, update, or delete.

For example, if you cache a list of products with the tag `Product:list:*` and a new product is created, the Caching Module automatically invalidates the cached list of products.

This ensures that your application always serves fresh data and avoids serving stale or outdated information.

The following table shows when the Caching Module invalidates cached data based on different database operations:

|Database Operation|Invalidated Cache Tags|
|---|---|
|Create|\`Entity:list:\*\`|
|Update|\`Entity:\{id}\`|
|Delete|\`Entity:list:\*\`|

### Which Data is Automatically Invalidated?

The Caching Module automatically invalidates your cached data when:

1. The data includes an `id` field. This is used internally to map the data to the corresponding cache tags.
   - When retrieving data with Query or the Index Module, ensure the `id` field is included in the `fields` option.

```ts
const { data: products } = useQueryGraphStep({
  entity: "product",
  fields: ["id", "title"], // Ensure 'id' is included, or pass '*'
  options: {
    cache: {
      enable: true,
    },
  },
})
```

- When caching custom data with the Caching Module's service, ensure the data object includes an `id` property.

2. Custom tags follow the [Caching Tags Convention](#caching-tags-convention).

```ts
const data = { id: "prod_123", title: "Product 123" }
const key = await cachingModuleService.computeKey(data)
await cachingModuleService.set({
  key,
  tags: ["Product:prod_123", "Product:list:*"],
  data,
})
```

3. The `autoInvalidate` option is not set or is set to `true`. This option is enabled by default.

```ts
const { data: products } = useQueryGraphStep({
  entity: "product",
  fields: ["id", "title"],
  options: {
    cache: {
      enable: true,
      // This is enabled by default
      // autoInvalidate: true,
    },
  },
})
```

### Invalidation of Related Entities

If the cached data includes relations, and the relation is updated, the Caching Module also invalidates the cache tags for the related entity.

For example, consider the following Query usage:

```ts
const { data: products } = useQueryGraphStep({
  entity: "product",
  fields: ["id", "title", "variants.*"],
  options: {
    cache: {
      enable: true,
    },
  },
})
```

If the product's variant is updated, the Caching Module invalidates the cache tags for both the `Product` and `ProductVariant` entities.

### When to Disable Automatic Invalidation?

Disabling automatic invalidation means the data remains in the cache until it expires (based on the TTL) or is manually invalidated.

Consider disabling automatic invalidation in the following cases:

1. You're caching data that rarely changes, such as a list of countries.
2. You want to manage cache invalidation manually, such as when caching data as part of a custom workflow where you want to control when the cache is invalidated.
3. You're caching data that does not belong to a Medusa data model, such as data from an external API or computed values. This data is not automatically invalidated by default.

Disable automatic invalidation by setting the `autoInvalidate` option to `false` when caching data with Query, the Index Module, or the Caching Module's service.

When you disable automatic invalidation, manually [invalidate](https://docs.medusajs.com/references/caching-service#clear/index.html.md) the cached data when it changes.

***

## Caching Best Practices

### Cache Rarely-Changing Data

Cache data that is read frequently but changes infrequently, such as product information, categories, or static content.

Caching such data can significantly improve performance and reduce database load.

### Do Not Cache Dynamic Data

Avoid caching data that changes frequently or is user-specific, such as shopping cart contents, user sessions, or product pricing.

Caching such data can lead to inconsistencies and stale information being served to users. It also increases bandwidth and memory usage, as the cache is updated frequently.

### Do Not Cache Frequently Updated Data

Avoid caching data that is updated frequently, such as inventory levels or order statuses.

Caching such data increases the overhead of cache invalidation and may lead to performance degradation.


# How to Clear Cached Data

In this guide, you'll learn how to clear cached data in Medusa.

## Why Clear Cache?

You should mainly cache data that isn't frequently changing, such as product details or categories. However, there are scenarios where you might need to clear the cache of that data manually.

For example, if you've integrated a third-party system that updates product information outside of Medusa, or if you've cached data from the third-party system, Medusa won't be aware of changes made externally.

In such cases, you should clear the cache in Medusa to ensure it fetches the most up-to-date information from the source rather than relying on outdated cached data.

***

## How to Clear Cache

This section explains how to clear data cached by the Caching Module in Medusa.

### Identify Cache Tags

Before clearing the cache, identify the specific cache tags associated with the data you want to clear.

When you cache entities with the [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) or [Index Module](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index-module/index.html.md), the Caching Module automatically generates tags based on the entity type and its ID:

- `Entity:id`: Cache tag for a single record of an entity. For example, `Product:prod_123` caches a single product with the ID `prod_123`.
- `Entity:list:*`: Cache tag for a list of records of an entity. For example, `Product:list:*` caches a list of products.

To clear the cache for a specific product, use the `Product:{id}` tag. To clear the cache for all products, use the `Product:list:*` tag.

Refer to the [Caching Module Concepts guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching/concepts#caching-tags-convention/index.html.md) to learn more about cache tags.

### Clear Cache with Caching Module Service

To clear cached data, use the [clear](https://docs.medusajs.com/references/caching-service#clear/index.html.md) method of the Caching Module's service. You can resolve the service in a workflow step, API route, subscriber, or scheduled job, then call the `clear` method with the identified cache tags.

For example, to clear the cache for specific products in a workflow step:

```ts
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

type ClearProductCacheInput = {
  productId: string | string[]
}

export const clearProductCacheStep = createStep(
  "clear-product-cache",
  async ({ productId }: ClearProductCacheInput, { container }) => {
    const cachingModuleService = container.resolve(Modules.CACHING)

    const productIds = Array.isArray(productId) ? productId : [productId]

    // Clear cache for all specified products
    for (const id of productIds) {
      if (id) {
        await cachingModuleService.clear({
          tags: [`Product:${id}`],
        })
      }
    }

    return new StepResponse({})
  }
)
```

In this example, the `clearProductCacheStep` step takes a `productId` (or an array of IDs) as input and clears the cache for each specified product using its cache tag.

You can then use this step in a workflow to clear the cache whenever necessary, such as after receiving a webhook from a third-party system indicating that product data has changed.


# Create Memcached Caching Module Provider

In this tutorial, you'll learn how to create a Memcached [Caching Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching/providers/index.html.md) for your Medusa application.

[Memcached](https://memcached.org/) is a high-performance, distributed memory caching system that speeds up dynamic web applications by reducing database load.

By the end of this tutorial, you'll be able to cache data in your Medusa application using Memcached.

Refer to the [Caching Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching/index.html.md) documentation to learn more about caching in Medusa.

![Diagram illustrating the Memcached caching module provider in a Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1759925175/Medusa%20Resources/memcached-summary_ztlanl.jpg)

[Full Code](https://github.com/medusajs/examples/tree/main/memcached-caching): Find the complete code on GitHub

***

### Prerequisites



## 1. Install Memcached Client

To interact with Memcached, you'll need to install the `memjs` client. You can install it in your Medusa project by running the following command:

```bash npm2yarn
npm install memjs
```

***

## 2. Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/memcached` in your Medusa project.

***

## 3. Create Memcached Connection Loader

Next, establish a connection to Memcached in your module using a [Loader](https://docs.medusajs.com/docs/learn/fundamentals/modules/loaders/index.html.md). A loader is an asynchronous function that runs when the module is initialized. It's useful for setting up connections to external services, such as databases or caching systems.

Create the file `src/modules/memcached/loaders/connection.ts` with the following content:

```ts title="src/modules/memcached/loaders/connection.ts" highlights={loaderHighlights}
import { LoaderOptions } from "@medusajs/framework/types"
import * as memjs from "memjs"

export type ModuleOptions = {
  serverUrls?: string[]
  username?: string
  password?: string
  options?: memjs.ClientOptions
  cachePrefix?: string
  defaultTtl?: number // Default TTL in seconds
  compression?: {
    enabled?: boolean
    threshold?: number // Minimum size in bytes to compress
    level?: number // Compression level (1-9)
  }
}

export default async function connection({
  container,
  options,
}: LoaderOptions<ModuleOptions>) {
  const logger = container.resolve("logger")
  const { 
    serverUrls = ["127.0.0.1:11211"], 
    username, 
    password, 
    options: clientOptions,
  } = options || {}

  try {
    logger.info("Connecting to Memcached...")

    // Create Memcached client
    const client = memjs.Client.create(serverUrls.join(","), {
      username,
      password,
      ...clientOptions,
    })

    // Test the connection
    await new Promise<void>((resolve, reject) => {
      client.stats((err, stats) => {
        if (err) {
          logger.error("Failed to connect to Memcached:", err)
          reject(err)
        } else {
          logger.info("Successfully connected to Memcached")
          resolve()
        }
      })
    })

    // Register the client in the container
    container.register({
      memcachedClient: {
        resolve: () => client,
      },
    })

  } catch (error) {
    logger.error("Failed to initialize Memcached connection:", error)
    throw error
  }
}
```

You first define module options that are passed to the Memcached Module Provider. You'll set those up later when you [register the module in your Medusa application](#register-memcached-module-provider). The module accepts the following options:

- `serverUrls`: An array of Memcached server URLs. Defaults to `["127.0.0.1:11211"]`.
- `username`: The username for authenticating with the Memcached server (if required).
- `password`: The password for authenticating with the Memcached server (if required).
- `options`: Additional options to pass to the Memcached client.
- `cachePrefix`: A prefix to use for all cache keys to avoid collisions. Defaults to `"medusa"`.
- `defaultTtl`: The default time-to-live (TTL) for cached items, in seconds. Defaults to `3600` (1 hour).
- `compression`: Configuration for data compression:
  - `enabled`: Whether to enable compression. Defaults to `true`.
  - `threshold`: The minimum size in bytes for data to be compressed. Defaults to `2048` (2KB).
  - `level`: The compression level (1-9). Defaults to `6`.

Then, export a loader function. This function receives an object with the following properties:

- `container`: The [Module container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) that holds Framework and module-specific resources.
- `options`: The options passed to the module when it's registered in the Medusa application.

In the loader, you create a Memcached client and test the connection. If the connection is successful, you register the client in the container, allowing you later to access it in the module's service.

***

## 4. Create Memcached Module Provider Service

You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can interact with third-party services to perform operations.

In this step, you'll create the service of the Memcached Module Provider. This service must implement the `ICachingProviderService` and implement its methods.

To create the service, create the file `src/modules/memcached/service.ts` with the following content:

```ts title="src/modules/memcached/service.ts" highlights={serviceHighlights1}
import { ICachingProviderService } from "@medusajs/framework/types"
import * as memjs from "memjs"
import { ModuleOptions } from "./loaders/connection"

type InjectedDependencies = {
  memcachedClient: memjs.Client
}

class MemcachedCachingProviderService implements ICachingProviderService {
  static identifier = "memcached-cache"

  protected client: memjs.Client
  protected options_: ModuleOptions
  protected readonly CACHE_PREFIX: string
  protected readonly TAG_PREFIX: string
  protected readonly OPTIONS_PREFIX: string
  protected readonly KEY_TAGS_PREFIX: string
  protected readonly TAG_KEYS_PREFIX: string
  protected readonly compressionEnabled: boolean
  protected readonly compressionThreshold: number
  protected readonly compressionLevel: number
  protected readonly defaultTtl: number

  constructor(
    { memcachedClient }: InjectedDependencies,
    options: ModuleOptions
  ) {
    this.client = memcachedClient
    this.options_ = options
    
    // Set all prefixes with the main prefix
    const mainPrefix = options.cachePrefix || "medusa"
    this.CACHE_PREFIX = `${mainPrefix}:`
    this.TAG_PREFIX = `${mainPrefix}:tag:`
    this.OPTIONS_PREFIX = `${mainPrefix}:opt:`
    this.KEY_TAGS_PREFIX = `${mainPrefix}:key_tags:`
    this.TAG_KEYS_PREFIX = `${mainPrefix}:tag_keys:`
    
    // Set compression options
    this.compressionEnabled = options.compression?.enabled ?? true
    this.compressionThreshold = options.compression?.threshold ?? 2048 // 2KB default
    this.compressionLevel = options.compression?.level ?? 6 // Balanced compression
    
    // Set default TTL
    this.defaultTtl = options.defaultTtl ?? 3600 // 1 hour default
  }
}
```

You create the service that implements the `ICachingProviderService` interface. You define in the class some protected properties to hold dependencies, the Memcached client, and configuration options.

You also define a constructor in the service. Service constructors accept two parameters:

1. The Module container, from which you can resolve dependencies. In this case, you resolve the `memcachedClient` that you registered in the loader.
2. The options passed to the module when it's registered in the Medusa application.

You'll get a type error at this point because you haven't implemented the methods of the `ICachingProviderService` interface yet. You'll implement them next, along with utility methods.

### Compression Utility Methods

Before implementing the caching methods, you'll implement two utility methods to handle data compression and decompression.

These methods use the `zlib` library to compress data before storing it in Memcached and decompress it when retrieving it. This optimizes storage and network usage, especially for large data.

First, add the following imports at the top of the file:

```ts title="src/modules/memcached/service.ts"
import { deflate, inflate } from "zlib"
import { promisify } from "util"

const deflateAsync = promisify(deflate)
const inflateAsync = promisify(inflate)
```

Then, add the following methods to the `MemcachedCachingProviderService` class:

```ts title="src/modules/memcached/service.ts"
class MemcachedCachingProviderService implements ICachingProviderService {
  // ...
  private async compressData(data: string): Promise<{ 
    data: string; 
    compressed: boolean
  }> {
    if (!this.compressionEnabled || data.length < this.compressionThreshold) {
      return { data, compressed: false }
    }

    const buffer = Buffer.from(data, "utf8")
    const compressed = await deflateAsync(buffer)
    const compressedData = compressed.toString("base64")
    
    // Only use compression if it actually reduces size
    if (compressedData.length < data.length) {
      return { data: compressedData, compressed: true }
    }
    
    return { data, compressed: false }
  }

  private async decompressData(
    data: string, 
    compressed: boolean
  ): Promise<string> {
    if (!compressed) {
      return data
    }

    const buffer = Buffer.from(data, "base64")
    const decompressed = await inflateAsync(buffer)
    return decompressed.toString("utf8")
  }
}
```

You define two private methods:

- `compressData`: Takes a string `data` as input and compresses it using `zlib.deflate` if compression is enabled and the data size exceeds the defined threshold. It returns an object containing the (possibly compressed) data and a boolean indicating whether compression was applied.
- `decompressData`: Takes a string `data` and a boolean `compressed` as input. If `compressed` is `true`, it decompresses the data using `zlib.inflate`. Otherwise, it returns the data as is.

### Set Methods

Next, you'll implement the [set method](https://docs.medusajs.com/references/caching-module-provider#set/index.html.md) of the `ICachingProviderService` interface. This method stores a value in the cache with an optional time-to-live (TTL) and associated tags.

#### setKeyTags Helper Method

Before implementing the method, you'll implement the helper method `setKeyTags`. Since Memcached doesn't support tagging natively, you'll need to manage tags manually by storing mappings between keys and tags.

Add the following method to the `MemcachedCachingProviderService` class:

```ts title="src/modules/memcached/service.ts"
class MemcachedCachingProviderService implements ICachingProviderService {
  // ...
  private async setKeyTags(
    key: string, 
    tags: string[], 
    setOptions: memjs.InsertOptions
  ): Promise<void> {
    const timestamp = Math.floor(Date.now() / 1000)
    const tagNamespaces: Record<string, string> = {}
    const operations: Promise<any>[] = []

    // Batch all namespace operations
    for (const tag of tags) {
      const tagKey = this.TAG_PREFIX + tag
      const tagKeysKey = `${this.TAG_KEYS_PREFIX}${tag}`
      
      // Get namespace version
      operations.push(
        (async () => {
          const result = await this.client.get(tagKey)
          if (!result.value) {
            tagNamespaces[tag] = timestamp.toString()
            await this.client.add(tagKey, timestamp.toString())
          } else {
            tagNamespaces[tag] = result.value.toString()
          }
        })()
      )

      // Add key to tag's key list
      operations.push(
        (async () => {
          const result = await this.client.get(tagKeysKey)
          let keys: string[] = []
          if (result.value) {
            keys = JSON.parse(result.value.toString()) as string[]
          }
          if (!keys.includes(key)) {
            keys.push(key)
            await this.client.set(tagKeysKey, JSON.stringify(keys), setOptions)
          }
        })()
      )
    }

    await Promise.all(operations)
    
    // Store the tag namespaces for this key
    const keyTagsKey = `${this.KEY_TAGS_PREFIX}${key}`
    const serializedTags = JSON.stringify(tagNamespaces)
    await this.client.set(keyTagsKey, serializedTags, setOptions)
  }
}
```

The `setKeyTags` method takes a cache `key`, an array of `tags`, and Memcached `setOptions`.

In the method, you:

1. Get the current timestamp to use as a namespace version for tags.
2. Iterate over the provided tags and for each tag:
   - Retrieve the current namespace version from Memcached. If it doesn't exist, set it to the current timestamp.
   - Retrieve the list of keys associated with the tag. If the current key is not in the list, add it and update the list in Memcached.
3. Store the mapping of tags and their namespace versions for the given key.

You'll use this method in the `set` method to manage tags when storing a value.

#### set Method

Add the following `set` method to the `MemcachedCachingProviderService` class:

```ts title="src/modules/memcached/service.ts"
class MemcachedCachingProviderService implements ICachingProviderService {
  // ...
  async set({
    key,
    data,
    ttl,
    tags,
    options,
  }: {
    key: string
    data: any
    ttl?: number
    tags?: string[]
    options?: { autoInvalidate?: boolean }
  }): Promise<void> {
    const prefixedKey = this.CACHE_PREFIX + key
    const serializedData = JSON.stringify(data)
    const setOptions: memjs.InsertOptions = {}
    
    // Use provided TTL or default TTL
    setOptions.expires = ttl ?? this.defaultTtl

    // Compress data if enabled
    const { 
      data: finalData, 
      compressed,
    } = await this.compressData(serializedData)

    // Batch operations for better performance
    const operations: Promise<any>[] = [
      this.client.set(prefixedKey, finalData, setOptions),
    ]

    // Always store options (including compression flag) to allow checking them later
    const optionsKey = this.OPTIONS_PREFIX + key
    const optionsData = { ...options, compressed }
    operations.push(
      this.client.set(optionsKey, JSON.stringify(optionsData), setOptions)
    )

    // Handle tags using namespace simulation with batching
    if (tags && tags.length > 0) {
      operations.push(this.setKeyTags(key, tags, setOptions))
    }

    await Promise.all(operations)
  }
}
```

The `set` method takes an object with the following properties:

- `key`: The cache key.
- `data`: The value to cache.
- `ttl`: Optional time-to-live for the cached value, in seconds.
- `tags`: Optional array of tags to associate with the cached value.
- `options`: Optional additional options, such as `autoInvalidate`, which indicates whether to automatically invalidate the cache based on tags.

In the method, you:

1. Store the value in Memcached with the specified TTL. You store the compressed data if compression is enabled and necessary.
2. Store the options (including whether the data was compressed) in a separate key to allow checking them later.
   - This is necessary to determine whether the data can be automatically invalidated based on tags.
3. If tags are provided, call the `setKeyTags` method to set up the tag mappings.

You batch all operations using `Promise.all` for better performance.

### Get Methods

Next, you'll implement the [get method](https://docs.medusajs.com/references/caching-module-provider#get/index.html.md) of the `ICachingProviderService` interface. This method retrieves a value from the cache either by its key or by associated tags.

Before implementing the method, you need two helper methods.

#### validateKeyByTags Helper Method

The first helper method validates that a key is still valid based on its tags. You'll use this method when retrieving a value by its key to ensure that the value hasn't been invalidated by tag updates.

Add the following method to the `MemcachedCachingProviderService` class:

```ts title="src/modules/memcached/service.ts"
class MemcachedCachingProviderService implements ICachingProviderService {
  // ...
  private async validateKeyByTags(key: string, tags: string[]): Promise<boolean> {
    if (!tags || tags.length === 0) {
      return true // No tags to validate
    }

    // Get the stored tag namespaces for this key
    const keyTagsKey = `${this.KEY_TAGS_PREFIX}${key}`
    const keyTagsResult = await this.client.get(keyTagsKey)
    
    if (!keyTagsResult.value) {
      return true // No stored tags, assume valid
    }

    const storedTags = JSON.parse(keyTagsResult.value.toString())
    
    // Batch all namespace checks for better performance
    const tagKeys = Object.keys(storedTags).map((tag) => this.TAG_PREFIX + tag)
    const tagResults = await Promise.all(
      tagKeys.map((tagKey) => this.client.get(tagKey))
    )

    // Check if any tag namespace is missing or changed
    for (let i = 0; i < tagResults.length; i++) {
      const tag = Object.keys(storedTags)[i]
      const tagResult = tagResults[i]
      
      if (tagResult.value) {
        const currentTag = tagResult.value.toString()
        // If the namespace has changed since the key was stored, it's invalid
        if (currentTag !== storedTags[tag]) {
          return false
        }
      } else {
        // Namespace doesn't exist - this means it was reclaimed after being incremented
        // This indicates the tag was cleared, so the key should be considered invalid
        return false
      }
    }
    
    return true
  }
}
```

The `validateKeyByTags` method accepts a cache `key` and an array of `tags`.

In the method, you:

- Retrieve from Memcached the tag namespaces for the given key.
  - If no tags are stored, you assume the key is valid and return `true`.
- For each stored tag, retrieve its current namespace version from Memcached.
- Compare the current namespace version with the stored version. If any tag's namespace has changed or is missing, return `false`, indicating that the key is invalid. Otherwise, return `true`.

#### getByTags Helper Method

The second helper method retrieves the cached data associated with specified tags. You'll use this method when retrieving a value by tags.

Add the following method to the `MemcachedCachingProviderService` class:

```ts title="src/modules/memcached/service.ts"
class MemcachedCachingProviderService implements ICachingProviderService {
  // ...
  private async getByTags(tags: string[]): Promise<any[] | null> {
    if (!tags || tags.length === 0) {
      return null
    }

    // Get all keys associated with each tag
    const tagKeysOperations = tags.map((tag) => {
      const tagKeysKey = `${this.TAG_KEYS_PREFIX}${tag}`
      return this.client.get(tagKeysKey)
    })

    const tagKeysResults = await Promise.all(tagKeysOperations)
    
    // Collect all unique keys from all tags
    const allKeys = new Set<string>()
    for (const result of tagKeysResults) {
      if (result.value) {
        const keys = JSON.parse(result.value.toString()) as string[]
        keys.forEach((key) => allKeys.add(key))
      }
    }

    if (allKeys.size === 0) {
      return null
    }

     // Get all cached data for the collected keys
     const dataOperations = Array.from(allKeys).map(async (key) => {
       const prefixedKey = this.CACHE_PREFIX + key
       const result = await this.client.get(prefixedKey)
       
       if (!result.value) {
         return { key, data: null }
       }
       
       const dataString = result.value.toString()
       
       // Check if data is compressed
       const optionsKey = this.OPTIONS_PREFIX + key
       const optionsResult = await this.client.get(optionsKey)
       let compressed = false
       
       if (optionsResult.value) {
         const options = JSON.parse(optionsResult.value.toString())
         compressed = options.compressed || false
       }
       
       // Decompress if needed
       const decompressedData = await this.decompressData(dataString, compressed)
       return { key, data: JSON.parse(decompressedData) }
     })

    const dataResults = await Promise.all(dataOperations)
    
    // Filter out null data and validate tags for each key
    const validData: any[] = []
    for (const { key, data } of dataResults) {
      if (data !== null) {
        // Validate that this key is still valid for the requested tags
        const isValid = await this.validateKeyByTags(key, tags)
        if (isValid) {
          validData.push(data)
        }
      }
    }

    return Object.keys(validData).length > 0 ? validData : null
  }
}
```

The `getByTags` method takes an array of `tags`.

In the method, you:

- Retrieve from Memcached all keys associated with each tag. You collect all unique keys from the results.
- For each unique key, retrieve the cached data with its options.
  - If the data is compressed, decompress it using the `decompressData` method.
- Validate each key using the `validateKeyByTags` method to ensure that the data is still valid based on its tags.
- Return an array of valid cached data or `null` if no valid data is found.

You can now implement the `get` method using these helper methods.

#### get Method

Add the `get` method to the `MemcachedCachingProviderService` class:

```ts title="src/modules/memcached/service.ts"
class MemcachedCachingProviderService implements ICachingProviderService {
  // ...
  async get({
    key,
    tags,
  }: {
    key?: string
    tags?: string[]
  }): Promise<any> {
    if (key) {
      const prefixedKey = this.CACHE_PREFIX + key
      
      // Get the stored tags for this key and validate them
      const keyTagsKey = `${this.KEY_TAGS_PREFIX}${key}`
      const keyTagsResult = await this.client.get(keyTagsKey)
      
      if (keyTagsResult.value) {
        const storedTags = JSON.parse(keyTagsResult.value.toString())
        const tagNames = Object.keys(storedTags)
        
        const isValid = await this.validateKeyByTags(key, tagNames)
        if (!isValid) {
          return null
        }
      }

      const result = await this.client.get(prefixedKey)
      if (result.value) {
        const dataString = result.value.toString()
        
        // Check if data is compressed (look for compression flag in options)
        const optionsKey = this.OPTIONS_PREFIX + key
        const optionsResult = await this.client.get(optionsKey)
        let compressed = false
        
        if (optionsResult.value) {
          const options = JSON.parse(optionsResult.value.toString())
          compressed = options.compressed || false
        }
        
        // Decompress if needed
        const decompressedData = await this.decompressData(dataString, compressed)
        return JSON.parse(decompressedData)
      }
      return null
    }

    if (tags && tags.length > 0) {
      // Retrieve data by tags - get all keys associated with the tags
      return await this.getByTags(tags)
    }

    return null
  }
}
```

The `get` method takes an object with optional `key` and `tags` properties.

In the method:

- If a `key` is provided, you give it a higher priority and retrieve the cached value for that key.
  - You first check if the key has associated tags and validate them using the `validateKeyByTags` method, ensuring the cached value is still valid.
  - You decompress the data if it was stored in a compressed format.
  - If the key is not found or is invalid, you return `null`. Otherwise, you return the cached value.
- If no `key` is provided but `tags` are, you call the `getByTags` method to retrieve all cached values associated with the provided tags.
- If neither `key` nor `tags` are provided, you return `null`.

### Clear Methods

Finally, you'll implement the [clear method](https://docs.medusajs.com/references/caching-module-provider#clear/index.html.md) of the `ICachingProviderService` interface.

The `clear` method removes a cached value either by its key or by associated tags. It also receives an optional `options` parameter to control whether to automatically invalidate the cache based on tags:

- If `options` isn't set, you clear all cached values associated with the provided tags.
- If `options.autoInvalidate` is `true`, you only invalidate the keys of the provided tags whose options allow automatic invalidation.

Before implementing the `clear` method, you'll implement four helper methods to handle tag and key invalidation.

#### removeKeysFromTag Helper Method

The `removeKeysFromTag` method removes a list of keys from a tag's key list in Memcached. This is useful when invalidating by key or when clearing keys associated with a tag.

Add the following method to the `MemcachedCachingProviderService` class:

```ts title="src/modules/memcached/service.ts"
class MemcachedCachingProviderService implements ICachingProviderService {
  // ...
  private async removeKeysFromTag(tag: string, keysToRemove: string[]): Promise<void> {
    const tagKeysKey = `${this.TAG_KEYS_PREFIX}${tag}`
    const tagKeysResult = await this.client.get(tagKeysKey)
    
    if (!tagKeysResult.value) {
      return // No keys to remove
    }
    
    let keys: string[] = JSON.parse(tagKeysResult.value.toString()) as string[]
    
    // Remove the specified keys
    keys = keys.filter((key) => !keysToRemove.includes(key))
    
    if (keys.length === 0) {
      // If no keys left, delete the tag keys entry
      await this.client.delete(tagKeysKey)
    } else {
      // Update the tag keys list
      await this.client.set(tagKeysKey, JSON.stringify(keys))
    }
  }
}
```

The `removeKeysFromTag` method takes a `tag` and an array of `keysToRemove`.

In the method, you:

1. Retrieve the list of keys associated with the tag from Memcached.
2. Filter out the keys that need to be removed.
3. If no keys are left, delete the tag's key list entry. Otherwise, update the list in Memcached.

#### clearByKey Helper Method

Next, you'll implement the `clearByKey` method. It removes a cached value by its key and updates the associated tags to remove the key from their key lists.

Add the following method to the `MemcachedCachingProviderService` class:

```ts title="src/modules/memcached/service.ts"
class MemcachedCachingProviderService implements ICachingProviderService {
  // ...
  private async clearByKey(key: string): Promise<void> {
    // Get the key's tags before deleting to clean up tag key lists
    const keyTagsKey = `${this.KEY_TAGS_PREFIX}${key}`
    const keyTagsResult = await this.client.get(keyTagsKey)
    
    const operations: Promise<any>[] = [
      this.client.delete(this.CACHE_PREFIX + key),
      this.client.delete(this.OPTIONS_PREFIX + key),
      this.client.delete(keyTagsKey),
    ]

    // If the key has tags, remove it from tag key lists
    if (keyTagsResult.value) {
      const storedTags = JSON.parse(keyTagsResult.value.toString())
      const tagNames = Object.keys(storedTags)
      
      // Batch tag cleanup operations
      const tagCleanupOperations = tagNames.map(async (tag) => {
        await this.removeKeysFromTag(tag, [key])
      })
      operations.push(...tagCleanupOperations)
    }

    await Promise.all(operations)
  }
}
```

The `clearByKey` method takes a cache `key`.

In the method, you:

1. Retrieve the tags associated with the key before deleting it.
2. Delete the cached value, its options, and the key's tag mapping from Memcached.
3. If the key has associated tags, call the `removeKeysFromTag` method for each tag to remove the key from their key lists.
4. Batch all operations using `Promise.all` for better performance.

#### clearByTags Helper Method

Next, you'll implement the `clearByTags` method. It removes all cached values associated with the provided tags. You'll use this method when the `options` parameter isn't set in the `clear` method.

Add the following method to the `MemcachedCachingProviderService` class:

```ts title="src/modules/memcached/service.ts"
class MemcachedCachingProviderService implements ICachingProviderService {
  // ...
  private async clearByTags(tags: string[]): Promise<void> {
    const operations = tags.map(async (tag) => {
      const tagKey = this.TAG_PREFIX + tag
      const result = await this.client.increment(tagKey, 1)
      if (result === null) {
        // Key doesn't exist, create it with current timestamp
        const timestamp = Math.floor(Date.now() / 1000)
        await this.client.add(tagKey, timestamp.toString())
      }
    })

    await Promise.all(operations)
  }
}
```

The `clearByTags` method takes an array of `tags`.

In the method, you loop over the tags to increment their namespace versions in Memcached. If a tag's namespace doesn't exist, you create it with the current timestamp.

By incrementing the namespace version, you effectively invalidate the tag and all associated keys, as they will no longer match the stored namespace versions. The namespace version will also be replaced in Memcached after being reclaimed.

Learn more about this invalidation strategy in [Memcached's documentation](https://docs.memcached.org/userguide/usecases/).

#### clearByTagsWithAutoInvalidate Helper Method

The `clearByTagsWithAutoInvalidate` method removes cached values associated with the provided tags, but only for keys whose options allow automatic invalidation. You'll use this method when the `options.autoInvalidate` parameter is `true` in the `clear` method.

Add the following method to the `MemcachedCachingProviderService` class:

```ts title="src/modules/memcached/service.ts"
class MemcachedCachingProviderService implements ICachingProviderService {
  // ...
  private async clearByTagsWithAutoInvalidate(tags: string[]): Promise<void> {
    for (const tag of tags) {
      // Get the list of keys associated with this tag
      const tagKeysKey = `${this.TAG_KEYS_PREFIX}${tag}`
      const tagKeysResult = await this.client.get(tagKeysKey)
      
      if (!tagKeysResult.value) {
        continue
      }
      
      const keys = JSON.parse(tagKeysResult.value.toString()) as string[]
      
      // Check each key's options and delete if autoInvalidate is true
      const keysToRemove: string[] = []
      for (const key of keys) {
        const optionsKey = `${this.OPTIONS_PREFIX}${key}`
        const optionsResult = await this.client.get(optionsKey)
        
        if (optionsResult.value) {
          const options = JSON.parse(optionsResult.value.toString())
          if (options.autoInvalidate) {
            // Delete the key and its associated data
            await this.client.delete(this.CACHE_PREFIX + key)
            await this.client.delete(optionsKey)
            await this.client.delete(`${this.KEY_TAGS_PREFIX}${key}`)
            keysToRemove.push(key)
          }
        }
      }
      
      // Remove deleted keys from the tag's key list
      if (keysToRemove.length > 0) {
        await this.removeKeysFromTag(tag, keysToRemove)
      }
    }
  }
}
```

You define the `clearByTagsWithAutoInvalidate` method, which takes an array of `tags`.

In the method, you loop over the tags to:

1. Retrieve the list of keys associated with each tag from Memcached.
2. For each key, retrieve its options and check if `autoInvalidate` is `true`.
3. If `autoInvalidate` is `true`, delete the cached value, its options, and its tag mapping from Memcached. You also keep track of the keys that were deleted.
4. After processing all keys for a tag, call the `removeKeysFromTag` method to remove the deleted keys from the tag's key list.

#### clear Method

Finally, add the `clear` method to the `MemcachedCachingProviderService` class:

```ts title="src/modules/memcached/service.ts"
class MemcachedCachingProviderService implements ICachingProviderService {
  // ...
  async clear({
    key,
    tags,
    options,
  }: {
    key?: string
    tags?: string[]
    options?: { autoInvalidate?: boolean }
  }): Promise<void> {
    if (key) {
      await this.clearByKey(key)
    }

    if (tags?.length) {
      if (!options) {
        // Clear all items with the specified tags
        await this.clearByTags(tags)
      } else if (options.autoInvalidate) {
        // Clear only items with autoInvalidate option set to true
        await this.clearByTagsWithAutoInvalidate(tags)
      }
    }
  }
}
```

The `clear` method takes an object with optional `key`, `tags`, and `options` properties.

In the method:

- If a `key` is provided, you call the `clearByKey` method to remove the cached value and update associated tags.
- If `tags` are provided:
  - If `options` isn't set, you call the `clearByTags` method to invalidate all cached values associated with the tags.
  - If `options.autoInvalidate` is `true`, you call the `clearByTagsWithAutoInvalidate` method to invalidate only the keys whose options allow automatic invalidation.

You've now implemented all methods of the `ICachingProviderService` interface in the `MemcachedCachingProviderService` class.

***

## 5. Export Memcached Module Provider Definition

The final piece of a module provider is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa which module this provider belongs to, its loaders, and its service.

Create the file `src/modules/memcached/index.ts` with the following content:

```ts title="src/modules/memcached/index.ts"
import { ModuleProvider, Modules } from "@medusajs/framework/utils"
import MemcachedCachingProviderService from "./service"
import connection from "./loaders/connection"

export default ModuleProvider(Modules.CACHING, {
  services: [MemcachedCachingProviderService],
  loaders: [connection],
})
```

You use the `ModuleProvider` function from the Modules SDK to create the module provider's definition. It accepts two parameters:

1. The module this provider belongs to. In this case, the `Modules.CACHING` module.
2. An object with the provider's `services` and `loaders`.

***

## 6. Register Memcached Module Provider

The last step is to register the Memcached Module Provider in your Medusa application.

### Enable Caching Feature Flag

First, enable the [Caching Module's feature flag](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching#1-enable-caching-feature-flag/index.html.md) by setting the following environment variable:

```bash
MEDUSA_FF_CACHING=true
```

### Register Memcached Module Provider

Then, in `medusa-config.ts`, add a new entry in the `modules` array to register the Memcached Module Provider:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/caching",
      options: {
        in_memory: {
          enable: true,
        },
        providers: [
          {
            resolve: "./src/modules/memcached",
            id: "caching-memcached",
            // Optional, makes this the default caching provider
            is_default: true,
            options: {
              serverUrls: process.env.MEMCACHED_SERVERS?.split(",") || 
                ["127.0.0.1:11211"],
              // add other optional options here...
            },
          },
          // other caching providers...
        ],
      },
    },
  ],
})
```

You register the `@medusajs/medusa/caching` module and add the Memcached Module Provider to its `providers` array.

You pass the options to configure the Memcached client and the module's behavior. These are the same [options you defined in the ModuleOptions type in the loader](#3-create-memcached-connection-loader).

### Add Environment Variables

Make sure you set the necessary environment variables in your `.env` file. For example:

```bash
MEMCACHED_SERVERS=127.0.0.1:11211 # Comma-separated list of Memcached server URLs
# Add other optional variables as needed
```

You set the `MEMCACHED_SERVERS` variable to specify the Memcached server URLs. You can also set other optional variables like `MEMCACHED_USERNAME` and `MEMCACHED_PASSWORD` based on your use case.

***

## Test the Memcached Caching Provider

To test that the Memcached Caching Provider is working, start the Medusa application with the following command:

```bash npm2yarn
npm run dev
```

You'll see in the logs that the Memcached connection is established successfully:

```bash
info:    Connecting to Memcached...
info:    Successfully connected to Memcached
```

If you set the `is_default` option to `true` in the provider registration, the Memcached Caching Provider will be used for all caching operations in the Medusa application.

### Create Test API Route

To verify that the caching is working, you can create a simple [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) that retrieves data with caching.

To create an API route, create a new file at `src/api/test-cache/route.ts` with the following content:

```ts title="src/api/test-cache/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
  const query = req.scope.resolve("query")
    
  // Test caching with a simple query
  const { data } = await query.graph({
    entity: "product",
    fields: ["id", "title", "handle"],
  }, {
    cache: {
      enable: true,
      // For testing purposes
      key: "test-cache-products",
      // If you didn't set is_default to true, uncomment the following line
      // providers: ["caching-memcached"],
    },
  })

  res.status(200).json({
    message: "Products retrieved with Memcached caching",
    data,
  })
}
```

This creates a `GET` route at `/test-cache` that retrieves products using Query with caching enabled. The `key` so that you can easily check the cached data in Memcached for testing purposes.

Then, send a `GET` request to the `/test-cache` endpoint:

```bash
curl http://localhost:9000/test-cache
```

You'll receive the list of products in the response.

You can then check that the data is cached in Memcached using the [memcached-cli](https://github.com/pd4d10/memcached-cli) tool.

First, establish a connection to your Memcached server:

```bash
npx memcached-cli localhost:11211 # Replace with your Memcached server URL
```

Then, retrieve the cached data using the key you specified in the API route:

```bash
get medusa:test-cache-products
```

Notice that you prefix the key with `medusa:`, which is the default prefix unless you set the `keyPrefix` option in the provider registration.

***

## Next Steps

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.


# Migrate from Cache Module to Caching Module

In this guide, you'll learn how to migrate from the deprecated [Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/index.html.md) to the new [Caching Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching/index.html.md).

The Caching Module is available starting [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0).

1. Set up a Cache Module in `medusa-config.ts`.
2. Used the Cache Module's service in their code.

If you haven't done either of these, you don't need to migrate to the Caching Module. You can refer to the [Caching Module guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching#install-the-caching-module/index.html.md) to learn how to set it up.

## Why Migrate to the Caching Module?

The Caching Module provides improved performance, flexibility, and scalability compared to the deprecated Cache Module.

It also offers a better developer experience, making it easier to cache Query results and other data.

Additionally, the Caching Module supports registering multiple caching providers, such as Redis and Memcached, allowing you to use different caching backends in your application.

***

## Architectural Module Changes

The Cache Module implements caching logic, including integration with third-party caching services like Redis. For example, the Redis Cache Module handled connecting to the Redis server and performing caching operations.

The Caching Module, on the other hand, follows a provider-based architecture. The Caching Module provides the interface you use to manage cached data, while Caching Module Providers implement the actual caching logic. You can choose which provider to use, such as the Redis Caching Module Provider or a custom provider you create.

This separation of concerns allows for greater flexibility and extensibility. You can easily switch between different caching providers or create your own custom provider without modifying the core Caching Module.

![Diagram illustrating the change in architecture from Cache Module to Caching Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1759845565/Medusa%20Resources/cache-to-caching_yxaped.jpg)

***

## How to Migrate to the Caching Module

### Prerequisites

- [Updated your Medusa application to v2.11.0 or later](https://github.com/medusajs/medusa/releases/tag/v2.11.0)

### 1. Remove the Cache Module

The first step is to remove the Cache Module from your `medusa-config.ts` file.

For example, if you set up the Redis Cache Module, remove the following code from `medusa-config.ts`:

```ts title="medusa-config.ts" highlights={[["5"], ["6"], ["7"], ["8"], ["9"], ["10"]]}
module.exports = defineConfig({
  // ...
  modules: [
    // REMOVE THE FOLLOWING LINES
    {
      resolve: "@medusajs/medusa/cache-redis",
      options: { 
        redisUrl: process.env.CACHE_REDIS_URL,
      },
    },
  ],
})
```

### 2. Install and Register the Caching Module

The Caching Module is installed by default in your application. To use it, enable the caching feature flag and register the module in your `medusa-config.ts` file.

Refer to the [Caching Module guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching#install-the-caching-module/index.html.md) for setup instructions.

### 3. (Optional) Update Your Code to Use the Caching Module's Service

If you're using the Cache Module's service in your code, update it to use the Caching Module's service instead.

#### Container Key Change

Previously, you resolved the Cache Module's service using the `Modules.CACHE` or `cache` key.

Now, use the `Modules.CACHING` or `caching` key to resolve the Caching Module's service. For example:

```ts
const cachingModuleService = container.resolve(Modules.CACHING)
// or
const cachingModuleService = container.resolve("caching")
```

#### Method Changes

The Caching Module's service has similar methods to the Cache Module's service:

1. `get` -> Use the Caching Module's [get method](https://docs.medusajs.com/references/caching-service#get/index.html.md).
2. `set` -> Use the Caching Module's [set method](https://docs.medusajs.com/references/caching-service#set/index.html.md).
3. `invalidate` -> Use the Caching Module's [clear method](https://docs.medusajs.com/references/caching-service#clear/index.html.md).

### 4. (Optional) Create Custom Caching Module Provider

If you have a custom Cache Module, recreate it as a custom Caching Module Provider. For example, recreate a custom Memcached Cache Module as a Caching Module Provider.

The Caching Module Provider's service has similar methods to the Cache Module's service. Refer to the [Create Caching Module Provider guide](https://docs.medusajs.com/references/caching-module-provider/index.html.md) for instructions on creating a custom Caching Module Provider.

***

## Understand Caching Changes

With the Cache Module, you handled caching and invalidation manually.

The Caching Module can now handle caching and invalidation automatically for certain operations, such as caching Query results. You can still use the Caching Module's service to cache custom data.

Learn more about the Caching Module in the [Caching Module guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching/index.html.md).


# Caching Module

In this guide, you'll learn about the Caching Module and its providers.

Refer to the [Medusa Cache Cloud](https://docs.medusajs.com/cloud/cache/index.html.md) guide for setting up the Caching Module in Cloud.

The Caching Module is available starting [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0). It replaces the deprecated [Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/index.html.md).

## What is the Caching Module?

The Caching Module provides functionality to cache data in your Medusa application, improving performance and reducing latency for frequently accessed data.

For example, Medusa uses the Caching Module to cache product information, and you can cache custom data such as brand information.

The Caching Module stores and retrieves cached data using the caching service you integrate, such as [Redis](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching/providers/redis/index.html.md) or [Memcached](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching/guides/memcached/index.html.md). This provides flexibility in customizing your Medusa application's infrastructure to meet your performance and scalability requirements.

![Diagram illustrating the Caching Module architecture](https://res.cloudinary.com/dza7lstvk/image/upload/v1759846791/Medusa%20Resources/caching-overview_tz91tw.jpg)

### Caching Module vs Cache Module

Before Medusa v2.11.0, you used the [Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/index.html.md) to cache data. The Cache Module is now deprecated and has been replaced by the Caching Module.

If you're using the Cache Module in your application, refer to the [migrate to the Caching Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching/migrate-cache/index.html.md).

***

## Install the Caching Module

### Prerequisites

- [Redis installed and Redis server running](https://redis.io/docs/getting-started/installation/)

The Caching Module is installed by default in your application. To use it, enable the caching feature flag and register the module in your `medusa-config.ts` file.

### 1. Enable Caching Feature Flag

The caching feature is currently behind a feature flag. To enable it, set the `MEDUSA_FF_CACHING` environment variable to `true` in your `.env` file:

```bash
MEDUSA_FF_CACHING=true
```

This enables you to use the Caching Module and activates caching features in Medusa's core.

### 2. Register the Caching Module

Next, add the Caching Module to the `modules` property of the exported object in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/caching",
      options: {
        providers: [
          {
            resolve: "@medusajs/caching-redis",
            id: "caching-redis",
            // Optional, makes this the default caching provider
            is_default: true,
            options: {
              redisUrl: process.env.CACHE_REDIS_URL,
              // more options...
            },
          },
        ],
      },
    },
  ],
})
```

This registers the Caching Module in your application with the [Redis Caching Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching/providers/redis/index.html.md).

The Caching Module requires at least one Caching Module Provider to be registered. If you do not register any providers, the Caching Module throws an error when your application starts.

### What is a Caching Module Provider?

A Caching Module Provider implements the underlying logic for caching data, such as integrating third-party caching services. The Caching Module then uses the registered Caching Module Provider to handle caching operations.

Refer to the [Caching Module Providers guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching/providers/index.html.md) to learn more about Caching Module Providers in Medusa, and how to configure the default provider.

- [Redis](https://docs.medusajs.com/infrastructure-modules/caching/providers/redis/index.html.md)
- [Memcached](https://docs.medusajs.com/infrastructure-modules/caching/guides/memcached/index.html.md)

### What Data is Cached by Default?

After you enable the Caching Module, Medusa automatically caches data for several business-critical APIs to boost performance and throughput. This includes all cart-related operations, where the following data is cached:

- Regions
- Promotion codes
- Variant price sets
- Variants
- Shipping options
- Sales channels
- Customers

***

## How to Use the Caching Module

You can cache data with the Caching Module in two ways: by caching data retrieved with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) or the [Index Module](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index-module/index.html.md), or by directly using the [Caching Module's service](https://docs.medusajs.com/references/caching-service/index.html.md).

### 1. Caching with Query or Index Module

You can cache results from Query and the Index Module by passing the `cache` option to the `query.graph` and `query.index` methods, or to the `useQueryGraphStep` in workflows.

For example:

```ts highlights={[["14"], ["15"], ["16"]]}
import {
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

export const workflow = createWorkflow(
  "workflow-1",
  () => {
    const { data: products } = useQueryGraphStep({
      entity: "product",
      fields: ["id", "title"],
      options: {
        cache: {
          enable: true,
        },
      },
    })

    return new WorkflowResponse(products)
  }
)
```

The `useQueryGraphStep` accepts an `options.cache` property that enables and configures caching of the results.

When caching is enabled, the Caching Module stores the results in the underlying caching service (for example, Redis). Subsequent calls to the same query retrieve the results from the cache, improving performance.

Query and the Index Module accept other caching options. Learn more in the [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#cache-query-results/index.html.md) and [Index Module](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index-module#cache-index-module-results/index.html.md) guides.

### 2. Caching with the Caching Module's Service

You can also use the Caching Module's service directly to cache custom data in your Medusa application.

For example, resolve the Caching Module's service in a workflow step and use its methods to cache brand information:

```ts highlights={cachingHighlights}
import { Modules } from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

type StepInput = {
  filters: Record<string, unknown>
}

export const getBrandStep = createStep(
  "get-brand-step",
  async (input: StepInput, { container }) => {
    const cachingModuleService = container.resolve(Modules.CACHING)
    const brandModuleService = container.resolve("brand")

    const cacheKey = await cachingModuleService.computeKey(input.filters)
    const cachedValue = await cachingModuleService.get({
      tags: ["brand"],
    })

    if (cachedValue) {
      return new StepResponse(cachedValue)
    }

    const brand = await brandModuleService.getBrand(input.filters)

    await cachingModuleService.set({
      key: cacheKey,
      tags: [`Brand:${brand.id}`],
      data: brand,
    })

    return new StepResponse(brand)
  }
)
```

In the example above, you create a step that resolves the Caching Module's service from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

Then, you use the `get` method of the service to retrieve a cached value with the tag `"brand"`. You can also use other methods such as `set` to cache a value and `clear` to remove a cached value.

Learn about other methods and options of the Caching Module in the [Use Caching Module](https://docs.medusajs.com/references/caching-service/index.html.md) guide.

### Which Caching Method Should You Use?

|Caching Method|When to Use|
|---|---|
|Query or Index Module|Ideal for standard data retrieval scenarios, such as fetching products or custom data.|
|Caching Module's Service|Suitable for caching computed values, external API responses, or to control caching behavior.|

Caching data with Query or the Index Module is ideal when retrieving standard data, such as products or custom entities. The Caching Module automatically handles cache keys and invalidation for you.

If you need to cache custom data like computed values or external API responses, or you want finer control over caching behavior, use the Caching Module's service directly. In this case, you must manage cache keys yourself, though you can still enable automatic invalidation using the service's `set` method.

***

## Caching Module Options

You can pass the following options to the Caching Module when registering it in your `medusa-config.ts` file:

|Option|Description|Default|
|---|---|---|
|\`ttl\`|A number indicating the default time-to-live (TTL) in seconds for cached items. After this period, cached items will be removed from the cache.
This number is passed to the underlying caching provider if no TTL is specified when caching data.|\`3600\`|
|\`providers\`|An array of caching providers to use. This allows you to configure multiple caching providers for different use cases.|No providers by default|

***

## Caching Concepts

To learn more about caching concepts such as cache keys, cache tags, and automatic cache invalidation, refer to the [Caching Module Concepts guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching/concepts/index.html.md).


# Caching Module Providers

In this guide, you'll learn about Caching Module Providers in Medusa, including how to configure and use them in your Medusa application.

## What is a Caching Module Provider?

A Caching Module Provider implements the logic for caching data, such as integrating third-party caching services. The [Caching Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching/index.html.md) then uses the registered Caching Module Providers to handle caching data.

Medusa provides the [Redis Caching Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching/providers/redis/index.html.md) that you can use in development and production. You can also [Create a Caching Provider](https://docs.medusajs.com/references/caching-module-provider/index.html.md).

- [Redis](https://docs.medusajs.com/infrastructure-modules/caching/providers/redis/index.html.md)
- [Memcached](https://docs.medusajs.com/infrastructure-modules/caching/guides/memcached/index.html.md)

***

## Default Caching Module Provider

You can register multiple Caching Module Providers and specify which one to use as the default. The Caching Module uses the default provider for all caching operations unless you specify a specific provider.

### How is the Default Provider Selected?

The Caching Module determines the default Caching Module Provider based on the following scenarios:

|Scenario|Default Provider|
|---|---|---|
|One provider is registered.|The registered provider.|
|Multiple providers and one of them has an |The provider with the |

If none of the above scenarios apply, the Caching Module throws an error during startup indicating that no default provider is configured.

### Setting the Default Caching Module Provider

To specify a provider as the default, you can set its `is_default` option to `true` when registering it in the `provider` array of the Caching Module.

For example:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/caching",
      options: {
        providers: [
          {
            id: "caching-redis",
            resolve: "@medusajs/caching-redis",
            is_default: true, // Set as the default provider
            options: {
              // Redis options...
            },
          },
          {
            id: "caching-memcached",
            resolve: "./path/to/your/memcached-provider",
            options: {
              // Memcached options...
            },
          },
        ],
      },
    },
  ],
})
```

In this example, the Redis Caching Module Provider is set as the default provider by setting `is_default: true`. The Memcached Caching Module Provider is also registered but not set as the default.

***

## Caching with Specific Providers

Whether you're caching data with Query, the Index Module, or directly using the Caching Module's service, you can specify an array of provider IDs to use for that specific caching operation.

For example, considering you have the [above configuration](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching#setting-the-default-caching-module-provider/index.html.md) with both Redis and Memcached providers registered, you can specify which provider to use when caching data:

### Query / Index Module

```ts highlights={[["7"]]}
const { data: products } = useQueryGraphStep({
  entity: "product",
  fields: ["id", "title"],
  options: {
    cache: {
      enable: true,
      providers: ["caching-memcached"], // Specify Memcached provider
    },
  },
})
```

### Caching Module Service

```ts highlights={[["6"]]}
const cachingModuleService = container.resolve(Modules.CACHING)

await cachingModuleService.set({
  key: "product-list",
  data: products,
  providers: ["caching-memcached"], // Specify Memcached provider
})
```

In this example, both Query and the Caching Module's service use the Memcached provider, overriding the default Redis provider.

The ID you pass is the same ID you specified in `medusa-config.ts` when registering the provider.


# Redis Caching Module Provider

The Redis Caching Module Provider is a robust caching solution that leverages [Redis](https://redis.io/) to store cached data. Redis offers high performance, scalability, and data persistence, making it an ideal choice for production environments.

The Caching Module and its providers are available starting [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0).

***

## Register the Redis Caching Module

### Prerequisites

- [Redis installed and Redis server running](https://redis.io/docs/getting-started/installation/)

To use the Redis Caching Module Provider, you need to register it in the `providers` array of the Caching Module in your `medusa-config.ts`.

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/caching",
      options: {
        providers: [
          {
            resolve: "@medusajs/caching-redis",
            id: "caching-redis",
            // Optional, makes this the default caching provider
            is_default: true,
            options: {
              redisUrl: process.env.CACHE_REDIS_URL,
              // more options...
            },
          },
          // other caching providers...
        ],
      },
    },
  ],
})
```

Notice that you pass an `id` property to the provider. The provider will be registered with that ID, which you can use to explicitly [specify the provider when caching data](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching/providers#how-to-use-the-caching-module/index.html.md).

### Environment Variables

Make sure to add the following environment variable:

```bash
CACHE_REDIS_URL=redis://localhost:6379
```

### Redis Caching Module Options

|Option|Description|Default|
|---|---|---|
|\`redisUrl\`|The connection URL for the Redis server.|Required. An error is thrown if not provided.|
|\`ttl\`|A number indicating the default time-to-live (TTL) in seconds for cached items. After this period, cached items will be removed from the cache.|\`3600\`|
|\`prefix\`|A string to prefix all cache keys with. This is useful for namespacing your cache keys, especially when sharing a Redis instance with other applications or modules.|No prefix by default|
|\`compressionThreshold\`|A number indicating the size threshold in bytes above which cached items will be compressed before being stored in Redis. This helps save memory when caching large items.|\`1024\`|

***

## Test the Redis Caching Module

You can test the Redis Caching Module by caching data using the Query or Index Module, or by directly using the Caching Module's service, as described in the [Caching Module guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching/providers#how-to-use-the-caching-module/index.html.md).

If you don't set the Redis Caching Module Provider as the default, you can explicitly specify its provider ID `caching-redis` when caching data with Query, the Index Module, or directly with the Caching Module's service.

`caching-redis` is the ID you set in the `medusa-config.ts` file when registering the Redis Caching Module Provider.

For example, you can create a workflow in `src/workflows/cache-products.ts` that caches products using the Redis Caching Module Provider:

```ts title="src/workflows/cache-products.ts" highlights={[["14"], ["15"], ["16"], ["17"]]}
import {
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows" 

export const cacheProductsWorkflow = createWorkflow(
  "cache-products",
  () => {
    const { data: products } = useQueryGraphStep({
      entity: "product",
      fields: ["id", "title"],
      options: {
        cache: {
          enable: true,
          providers: ["caching-redis"],
        },
      },
    })

    return new WorkflowResponse(products)
  }
)
```

Next, execute that workflow in an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). For example, create a route at `src/api/cache-product/route.ts` with the following content:

```ts title="src/api/cache-product/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { cacheProductsWorkflow } from "../../workflows/cache-products"

export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
  const { result } = await cacheProductsWorkflow(req.scope)
    .run({})

  res.status(200).json(result)
}
```

Finally, start your Medusa server with the following command:

```bash npm2yarn
npm run dev
```

Then, make a `GET` request to the `/cache-product` endpoint:

```bash
curl http://localhost:9000/cache-product
```

You should receive a response with the list of products. The first time you make this request, the products will be fetched from the database and cached in Redis. Subsequent requests will retrieve the products from the cache, resulting in improved performance.


# How to Create an Event Module

In this guide, you’ll learn how to create an Event Module.

## 1. Create Module Directory

Start by creating a new directory for your module. For example, `src/modules/my-event`.

***

## 2. Create the Event Service

Create the file `src/modules/my-event/service.ts` that holds the implementation of the event service.

The Event Module's main service must extend the `AbstractEventBusModuleService` class from the Medusa Framework:

```ts title="src/modules/my-event/service.ts"
import { AbstractEventBusModuleService } from "@medusajs/framework/utils"
import { Message } from "@medusajs/types"

class MyEventService extends AbstractEventBusModuleService {
  async emit<T>(data: Message<T> | Message<T>[], options: Record<string, unknown>): Promise<void> {
    throw new Error("Method not implemented.")
  }
  async releaseGroupedEvents(eventGroupId: string): Promise<void> {
    throw new Error("Method not implemented.")
  }
  async clearGroupedEvents(eventGroupId: string): Promise<void> {
    throw new Error("Method not implemented.")
  }
}

export default MyEventService
```

The service implements the required methods based on the desired publish/subscribe logic.

### eventToSubscribersMap\_ Property

The `AbstractEventBusModuleService` has a field `eventToSubscribersMap_`, which is a JavaScript Map. The map's keys are the event names, whereas the value of each key is an array of subscribed handler functions.

In your custom implementation, you can use this property to manage the subscribed handler functions:

```ts
const eventSubscribers = 
  this.eventToSubscribersMap_.get(eventName) || []
```

### emit Method

The `emit` method is used to push an event from the Medusa application into your messaging system. The subscribers to that event would then pick up the message and execute their asynchronous tasks.

An example implementation:

```ts title="src/modules/my-event/service.ts"
class MyEventService extends AbstractEventBusModuleService {
  async emit<T>(data: Message<T> | Message<T>[], options: Record<string, unknown>): Promise<void> {
    const events = Array.isArray(data) ? data : [data]

    for (const event of events) {
      console.log(`Received the event ${event.name} with data ${event.data}`)

      // TODO push the event somewhere
    }
  }
  // ...
}
```

The `emit` method receives the following parameters:

- data: (\`object or array of objects\`) The emitted event(s).

  - name: (\`string\`) The name of the emitted event.

  - data: (\`object\`) The data payload of the event.

  - metadata: (\`object\`) Additional details of the emitted event.

    - eventGroupId: (string) A group ID that the event belongs to.

  - options: (\`object\`) Additional options relevant for the event service.

### releaseGroupedEvents Method

Grouped events are useful when you have distributed transactions where you need to explicitly group, release, and clear events upon lifecycle transaction events.

If your Event Module supports grouped events, this method is used to emit all events in a group, then clear that group.

For example:

```ts title="src/modules/my-event/service.ts"
class MyEventService extends AbstractEventBusModuleService {
  protected groupedEventsMap_: Map<string, Message[]>

  constructor() {
    // @ts-ignore
    super(...arguments)

    this.groupedEventsMap_ = new Map()
  }

  async releaseGroupedEvents(eventGroupId: string): Promise<void> {
    const groupedEvents = this.groupedEventsMap_.get(eventGroupId) || []

    for (const event of groupedEvents) {
      const { options, ...eventBody } = event

      // TODO emit event
    }

    await this.clearGroupedEvents(eventGroupId)
  }

  // ...
}
```

The `releaseGroupedEvents` receives the group ID as a parameter.

In the example above, you add a `groupedEventsMap_` property to store grouped events. Then, in the method, you emit the events in the group, then clear the grouped events using the `clearGroupedEvents` which you'll learn about next.

To add events to the grouped events map, you can do it in the `emit` method:

```ts title="src/modules/my-event/service.ts"
class MyEventService extends AbstractEventBusModuleService {
  // ...
  async emit<T>(data: Message<T> | Message<T>[], options: Record<string, unknown>): Promise<void> {
    const events = Array.isArray(data) ? data : [data]

    for (const event of events) {
      console.log(`Received the event ${event.name} with data ${event.data}`)

      if (event.metadata.eventGroupId) {
        const groupedEvents = this.groupedEventsMap_.get(
          event.metadata.eventGroupId
        ) || []

        groupedEvents.push(event)

        this.groupedEventsMap_.set(event.metadata.eventGroupId, groupedEvents)
        continue
      }

      // TODO push the event somewhere
    }
  }
}
```

### clearGroupedEvents Method

If your Event Module supports grouped events, this method is used to remove the events of a group.

For example:

```ts title="src/modules/my-event/service.ts"
class MyEventService extends AbstractEventBusModuleService {
  // from previous section
  protected groupedEventsMap_: Map<string, Message[]>

  async clearGroupedEvents(eventGroupId: string): Promise<void> {
    this.groupedEventsMap_.delete(eventGroupId)
  }

  // ...
}
```

The method accepts the group's name as a parameter.

In the method, you delete the group from the `groupedEventsMap_` property (added in the previous section), deleting the stored events of it as well.

***

## 3. Create Module Definition File

Create the file `src/modules/my-event/index.ts` with the following content:

```ts title="src/modules/my-event/index.ts"
import MyEventService from "./service"
import { Module } from "@medusajs/framework/utils"

export default Module("my-event", {
  service: MyEventService,
})
```

This exports the module's definition, indicating that the `MyEventService` is the main service of the module.

***

## 4. Use Module

To use your Event Module, add it to the `modules` object exported as part of the configurations in `medusa-config.ts`. An Event Module is added under the `eventBus` key.

For example:

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/my-event",
      options: { 
        // any options
      },
    },
  ],
})
```


# Local Event Module

The Local Event Module uses Node EventEmitter to implement Medusa's pub/sub events system. The Node EventEmitter is limited to a single process environment.

This module is useful for development and testing, but it’s not recommended to be used in production.

For production, it’s recommended to use modules like [Redis Event Bus Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/redis/index.html.md).

***

## Register the Local Event Module

The Local Event Module is registered by default in your application.

Add the module into the `modules` property of the exported object in `medusa-config.ts`:

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/event-bus-local",
    },
  ],
})
```

***

## Test the Module

To test the module, start the Medusa application:

```bash npm2yarn
npm run dev
```

You'll see the following message in the terminal's logs:

```bash noCopy noReport
Local Event Bus installed. This is not recommended for production.
```


# Event Module

In this document, you'll learn what an Event Module is and how to use it in your Medusa application.

## What is an Event Module?

An Event Module implements the underlying publish/subscribe system that handles queueing events, emitting them, and executing their subscribers.

This makes the event architecture customizable, as you can either choose one of Medusa’s event modules or create your own.

Learn more about Medusa's event systems in the [Events and Subscribers documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).

### Default Event Module

By default, Medusa uses the [Local Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/local/index.html.md). This module uses Node’s EventEmitter to implement the publish/subscribe system. While this is suitable for development, it's recommended to use other Event Modules, such as the [Redis Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/redis/index.html.md), for production. You can also [Create an Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/create/index.html.md).

***

## How to Use the Event Module?

You can use the registered Event Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

Medusa provides the helper step [emitEventStep](https://docs.medusajs.com/references/helper-steps/emitEventStep/index.html.md) that you can use in your workflow. You can also resolve the Event Module's service in a step of your workflow and use its methods to emit events.

For example:

```ts
import { Modules } from "@medusajs/framework/utils"
import { 
  createStep,
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async ({}, { container }) => {
    const eventModuleService = container.resolve(
      Modules.EVENT_BUS
    )

    await eventModuleService.emit({
      name: "custom.event",
      data: {
        id: "123",
        // other data payload
      },
    })
  } 
)

export const workflow = createWorkflow(
  "workflow-1",
  () => {
    step1()
  }
)
```

In the example above, you create a workflow that has a step. In the step, you resolve the service of the Event Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

Then, you use the `emit` method of the Event Module to emit an event with the name `"custom.event"` and the data payload `{ id: "123" }`.

***

## List of Event Modules

Medusa provides the following Event Modules. You can use one of them, or [Create an Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/create/index.html.md).

- [Local](https://docs.medusajs.com/infrastructure-modules/event/local/index.html.md)
- [Redis](https://docs.medusajs.com/infrastructure-modules/event/redis/index.html.md)


# Redis Event Module

The Redis Event Module uses Redis to implement Medusa's pub/sub events system.

It's powered by [BullMQ](https://bullmq.io/) and `io-redis`. BullMQ is responsible for the message queue and worker, and `io-redis` is the underlying Redis client that BullMQ connects to for events storage.

In production, it's recommended to use this module.

Our Cloud offering automatically provisions a Redis instance and configures the Redis Event Module for you. Learn more in the [Redis](https://docs.medusajs.com/cloud/redis/index.html.md) Cloud documentation.

***

## Register the Redis Event Module

### Prerequisites

- [Redis installed and Redis server running](https://redis.io/docs/getting-started/installation/)

Add the module into the `modules` property of the exported object in `medusa-config.ts`:

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/event-bus-redis",
      options: { 
        redisUrl: process.env.EVENTS_REDIS_URL,
        // suggested additional options for production use
        jobOptions: {
          removeOnComplete: {
            // keep jobs for 1 hour or up to 1000 jobs
            age: 3600,
            count: 1000,
          },
          removeOnFail: {
            // keep jobs for 1 hour or up to 1000 jobs
            age: 3600,
            count: 1000,
          },
        },
      },
    },
  ],
})
```

### Environment Variables

Make sure to add the following environment variables:

```bash
EVENTS_REDIS_URL=<YOUR_REDIS_URL>
```

### Redis Event Module Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`redisUrl\`|A string indicating the Redis connection URL.|Yes|-|
|\`redisOptions\`|An object of Redis options. Refer to the |No|-|
|\`queueName\`|A string indicating BullMQ's queue name.|No|\`events-queue\`|
|\`queueOptions\`|An object of options to pass to the BullMQ constructor. Refer to |No|-|
|\`workerOptions\`|An object of options to pass to the BullMQ Worker constructor. Refer to |No|-|
|\`jobOptions\`|An object of options to pass to jobs added to the BullMQ queue. Refer to |No|-|

## Test the Module

To test the module, start the Medusa application:

```bash npm2yarn
npm run dev
```

You'll see the following message in the terminal's logs:

```bash noCopy noReport
Connection to Redis in module 'event-redis' established
```


# Local File Module Provider

The Local File Module Provider stores files uploaded to your Medusa application in the `/uploads` directory.

- The Local File Module Provider is only for development purposes. Use the [S3 File Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/s3/index.html.md) in production instead.
- The Local File Module Provider will only read files uploaded through Medusa. It will not read files uploaded manually to the `static` (or other configured) directory.

***

## Register the Local File Module

The Local File Module Provider is registered by default in your application.

Add the module into the `providers` array of the File Module:

The File Module accepts one provider only.

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = {
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/file",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/file-local",
            id: "local",
            options: {
              // provider options...
            },
          },
        ],
      },
    },
  ],
}
```

### Local File Module Options

|Option|Description|Default|
|---|---|---|---|---|
|\`upload\_dir\`|The directory to upload files to. Medusa exposes the content of the |\`static\`|
|\`backend\_url\`|The URL that serves the files.|\`http://localhost:9000/static\`|


# File Module

In this document, you'll learn about the File Module and its providers.

## What is the File Module?

The File Module exposes the functionalities to upload assets, such as product images, to the Medusa application. Medusa uses the File Module in its core commerce features for all file operations, and you can use it in your custom features as well.

***

## How to Use the File Module?

You can use the File Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

In a step of your workflow, you can resolve the File Module's service and use its methods to upload files, retrieve files, or delete files.

For example:

```ts
import { Modules } from "@medusajs/framework/utils"
import { 
  createStep,
  createWorkflow,
  StepResponse,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async ({}, { container }) => {
    const fileModuleService = container.resolve(
      Modules.FILE
    )

    const { url } = await fileModuleService.retrieveFile("image.png")

    return new StepResponse(url)
  } 
)

export const workflow = createWorkflow(
  "workflow-1",
  () => {
    const url = step1()

    return new WorkflowResponse(url)
  }
)
```

In the example above, you create a workflow that has a step. In the step, you resolve the service of the File Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

Then, you use the `retrieveFile` method of the File Module to retrieve the URL of the file with the name `"image.png"`. The URL is then returned as a response from the step and the workflow.

***

## What is a File Module Provider?

A File Module Provider implements the underlying logic of handling uploads and downloads of assets, such as integrating third-party services. The File Module then uses the registered File Module Provider to handle file operations.

Only one File Module Provider can be registered at a time. If you register multiple providers, the File Module will throw an error.

By default, Medusa uses the [Local File Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/local/index.html.md). This module uploads files to the `uploads` directory of your Medusa application.

This is useful for development. However, for production, it’s highly recommended to use other File Module Providers, such as the S3 File Module Provider. You can also [Create a File Provider](https://docs.medusajs.com/references/file-provider-module/index.html.md).

- [Local](https://docs.medusajs.com/infrastructure-modules/file/local/index.html.md)
- [AWS S3 (and Compatible APIs)](https://docs.medusajs.com/infrastructure-modules/file/s3/index.html.md)


# S3 File Module Provider

The S3 File Module Provider integrates Amazon S3 and services following a compatible API (such as MinIO or DigitalOcean Spaces) to store files uploaded to your Medusa application.

Cloud offers a managed file storage solution with AWS S3 for your Medusa application. Refer to the [S3](https://docs.medusajs.com/cloud/s3/index.html.md) Cloud documentation for more details.

## Prerequisites

### AWS S3

- [AWS account](https://console.aws.amazon.com/console/home?nc2=h_ct\&src=header-signin).
- Create [AWS user with AmazonS3FullAccess permissions](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-and-attach-iam-policy.html).
- Create [AWS user access key ID and secret access key](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_CreateAccessKey).
- Create [S3 bucket](https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-overview.html) with the "Public Access setting" enabled:
  1. On your bucket's dashboard, click on the Permissions tab.
  2. Click on the Edit button of the Block public access (bucket settings) section.
  3. In the form that opens, don't toggle any checkboxes and click the "Save changes" button.
  4. Confirm saving the changes by entering `confirm` in the pop-up that shows.
  5. Back on the Permissions page, scroll to the Object Ownership section and click the Edit button.
  6. In the form that opens:
     - Choose the "ACLs enabled" card.
     - Click on the "Save changes" button.
  7. Back on the Permissions page, scroll to the "Access Control List (ACL)" section and click on the Edit button.
  8. In the form that opens, enable the Read permission for "Everyone (public access)".
  9. Check the "I understand the effects of these changes on my objects and buckets." checkbox.
  10. Click on the "Save changes" button.

### MinIO

- Create [DigitalOcean account](https://cloud.digitalocean.com/registrations/new).
- Create [DigitalOcean Spaces bucket](https://docs.digitalocean.com/products/spaces/how-to/create/).
- Create [DigitalOcean Spaces access and secret access keys](https://docs.digitalocean.com/products/spaces/how-to/manage-access/#access-keys).

### DigitalOcean Spaces

1. Create a [Cloudflare account](https://dash.cloudflare.com/sign-up).
2. Set up your R2 bucket:
   - Navigate to R2 Object Storage in your dashboard. You may need to provide your credit-card information.
   - Click "Create bucket"
   - Enter a unique bucket name
   - Select "Automatic" for location
   - Choose "Standard" for storage class
   - Confirm by clicking "Create bucket"
3. Configure public access:
   - Make sure you have a [domain configured in your Cloudflare account](https://developers.cloudflare.com/dns/manage-dns-records/how-to/create-dns-records/).
   - On your bucket's dashboard, click on the Settings tab.
   - In the General Section look for Custom Domains (recommended for production use)
   - Click on the Add button to add your domain name.
   - Enter the domain name you want to connect to and select Continue.
   - Review the new record that will be added to the DNS table and select Connect Domain.
4. Retrieve credentials:
   - [Go to API tokens page](https://dash.cloudflare.com/?to=/:account/r2/api-tokens):
     - Click "Create User API token"
     - Edit the "R2 Token" name
     - Under Permissions, select Object Read & Write permission types
     - You can optionally specify the buckets that this API token has access to under the "Specify bucket(s)" section.
     - Once done, click the "Create User API Token" button.
     - Copy the jurisdiction-specific endpoint for S3 clients to S3\_ENDPOINT into your environment variables.
     - Copy the Access Key ID and Secret Access Key to the corresponding fields into your environment variables.
     - Copy your custom domain to `S3_FILE_URL` with leading https:// into your environment variables.

### Supabase S3 Storage

### Cloudflare R2

***

## Register the S3 File Module

Add the module into the `providers` array of the File Module:

The File Module accepts one provider only.

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = {
  // ...
  modules: [
    // ...
    {
      resolve: "@medusajs/medusa/file",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/file-s3",
            id: "s3",
            options: {
              file_url: process.env.S3_FILE_URL,
              access_key_id: process.env.S3_ACCESS_KEY_ID,
              secret_access_key: process.env.S3_SECRET_ACCESS_KEY,
              region: process.env.S3_REGION,
              bucket: process.env.S3_BUCKET,
              endpoint: process.env.S3_ENDPOINT,
              // other options...
            },
          },
        ],
      },
    },
  ],
}
```

### Additional Configuration for MinIO and Supabase

If you're using MinIO or Supabase, set `forcePathStyle` to `true` in the `additional_client_config` object.

For example:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/file",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/file-s3",
            id: "s3",
            options: {
              // ...
              additional_client_config: {
                forcePathStyle: true,
              },
            },
          },
        ],
      },
    },
  ],
})
```

### S3 File Module Options

|Option|Description|Default|
|---|---|---|---|---|
|\`file\_url\`|The base URL to upload files to.|-|
|\`access\_key\_id\`|The AWS or (S3 compatible) user's access key ID.|-|
|\`secret\_access\_key\`|The AWS or (S3 compatible) user's secret access key.|-|
|\`region\`|The bucket's region code.|-|
|\`bucket\`|The bucket's name.|-|
|\`endpoint\`|The URL to the AWS S3 (or compatible S3 API) server.|-|
|\`prefix\`|A string to prefix each uploaded file's name.|-|
|\`cache\_control\`|A string indicating how long objects remain in the AWS S3 (or compatible S3 API) cache.|\`public, max-age=31536000\`|
|\`download\_file\_duration\`|A number indicating the expiry time of presigned URLs in seconds.|\`3600\`|
|\`additional\_client\_config\`|Any additional configurations to pass to the S3 client.|-|

***

## Troubleshooting


# Locking Module

In this document, you'll learn about the Locking Module and its providers.

## What is the Locking Module?

The Locking Module manages access to shared resources by multiple processes or threads. It prevents conflicts between processes that are trying to access the same resource at the same time, and ensures data consistency.

Medusa uses the Locking Module to control concurrency, avoid race conditions, and protect parts of code that should not be executed by more than one process at a time. This is especially essential in distributed or multi-threaded environments.

For example, Medusa uses the Locking Module in inventory management to ensure that only one transaction can update the stock levels at a time. By using the Locking Module in this scenario, Medusa prevents overselling an inventory item and keeps its quantity amounts accurate, even during high traffic periods or when receiving concurrent requests.

***

## How to Use the Locking Module?

You can use the Locking Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

In a workflow, you can either use:

- Medusa's [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md) and [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md) to create locks around critical steps in your workflow.
- The Locking Module's service directly in your steps to have more control over the locking mechanism

For example:

### Workflow

```ts title="src/workflows/charge-customer.ts"
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
import { acquireLockStep, releaseLockStep } from "@medusajs/medusa/core-flows"
import { chargeCustomerStep } from "./steps/charge-customer-step"

type WorkflowInput = {
  customer_id: string;
  order_id: string;
}

export const chargeCustomerWorkflow = createWorkflow(
  "charge-customer",
  (input: WorkflowInput) => {
    acquireLockStep({
      key: input.order_id,
      // Attempt to acquire the lock for two seconds before timing out
      timeout: 2,
      // Lock is only held for a maximum of ten seconds
      ttl: 10,
    })

    chargeCustomerStep(input)

    releaseLockStep({
      key: input.order_id,
    })
  }
)
```

In the example above, you create a workflow that acquires a lock on an order using the `acquireLockStep` before charging a customer. The lock is then released using the `releaseLockStep` after the operation is complete.

This ensures that only one instance of the workflow can modify the order at a time, preventing issues like double charging the customer.

### Step

***

## When to Use the Locking Module?

You should use the Locking Module when you need to ensure that only one process can access a shared resource at a time. As mentioned in the inventory example previously, you don't want customers to order quantities of inventory that are not available, or to update the stock levels of an item concurrently.

In those scenarios, you can use the Locking Module to acquire a lock for a resource and execute a critical section of code that should not be accessed by multiple processes simultaneously.

***

## What is a Locking Module Provider?

A Locking Module Provider implements the underlying logic of the Locking Module. It manages the locking mechanisms and ensures that only one process can access a shared resource at a time.

Medusa provides [multiple Locking Module Providers](#list-of-locking-module-providers) that are suitable for development and production. You can also create a [custom Locking Module Provider](https://docs.medusajs.com/references/locking-module-provider/index.html.md) to implement custom locking mechanisms or integrate with third-party services.

### Default Locking Module Provider

By default, Medusa uses the In-Memory Locking Module Provider. This provider uses a plain JavaScript map to store the locks. While this is useful for development, it is not recommended for production environments as it is only intended for use in a single-instance environment.

To add more providers, you can register them in the `medusa-config.ts` file. For example:

```ts
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/locking",
      options: {
        providers: [
          // add providers here...
        ],
      },
    },
  ],
})
```

When you register other providers in `medusa-config.ts`, Medusa will set the default provider based on the following scenarios:

|Scenario|Default Provider|
|---|---|---|
|One provider is registered.|The registered provider.|
|Multiple providers are registered and none of them has an |In-Memory Locking Module Provider.|
|Multiple providers and one of them has an |The provider with the |

***

## List of Locking Module Providers

Medusa provides the following Locking Module Providers. You can use one of them, or [Create a Locking Module Provider](https://docs.medusajs.com/references/locking-module-provider/index.html.md).

- [Redis](https://docs.medusajs.com/infrastructure-modules/locking/redis/index.html.md)
- [PostgreSQL](https://docs.medusajs.com/infrastructure-modules/locking/postgres/index.html.md)


# PostgreSQL Locking Module Provider

The PostgreSQL Locking Module Provider uses PostgreSQL's advisory locks to control and manage locks across multiple instances of Medusa. Advisory locks are lightweight locks that do not interfere with other database transactions. By using PostgreSQL's advisory locks, Medusa can create distributed locks directly through the database.

The provider uses the existing PostgreSQL database in your application to manage locks, so you don't need to set up a separate database or service to manage locks.

While this provider is suitable for production environments, it's recommended to use the [Redis Locking Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/locking/redis/index.html.md) if possible.

***

## Register the PostgreSQL Locking Module Provider

To register the PostgreSQL Locking Module Provider, add it to the list of providers of the Locking Module in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/locking",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/locking-postgres",
            id: "locking-postgres",
            // set this if you want this provider to be used by default
            // and you have other Locking Module Providers registered.
            is_default: true,
          },
        ],
      },
    },
  ],
})
```

### Run Migrations

The PostgreSQL Locking Module Provider requires a new `locking` table in the database to store the locks. So, you must run the migrations after registering the provider:

```bash
npx medusa db:migrate
```

This will run the migration in the PostgreSQL Locking Module Provider and create the necessary table in the database.

***

## Use Provider with Locking Module

The PostgreSQL Locking Module Provider will be the default provider if you don't register any other providers, or if you set the `is_default` flag to `true`:

```ts title="medusa-config.ts" highlights={defaultHighlights}
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/locking",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/locking-postgres",
            id: "locking-postgres",
            is_default: true,
          },
        ],
      },
    },
  ],
})
```

If you use the Locking Module in your customizations, the PostgreSQL Locking Module Provider will be used by default in this case. You can also explicitly use this provider by passing its identifier `lp_locking-postgres` to the Locking Module's service methods.

For example, when using the `acquire` method in a [workflow step](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md):

```ts
import { Modules } from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async ({}, { container }) => {
    const lockingModuleService = container.resolve(
      Modules.LOCKING
    )

    await lockingModuleService.acquire("prod_123", {
      provider: "lp_locking-postgres",
    })
  } 
)
```


# Redis Locking Module Provider

The Redis Locking Module Provider uses Redis to manage locks across multiple instances of Medusa. Redis ensures that locks are globally available, which is ideal for distributed environments.

This provider is recommended for production environments where Medusa is running in a multi-instance setup.

Our Cloud offering automatically provisions a Redis instance and configures the Redis Locking Module Provider for you. Learn more in the [Redis](https://docs.medusajs.com/cloud/redis/index.html.md) Cloud documentation.

***

## Register the Redis Locking Module Provider

### Prerequisites

- [A redis server set up locally or a database in your deployed application.](https://redis.io/download)

To register the Redis Locking Module Provider, add it to the list of providers of the Locking Module in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/locking",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/locking-redis",
            id: "locking-redis",
            // set this if you want this provider to be used by default
            // and you have other Locking Module Providers registered.
            is_default: true,
            options: {
              redisUrl: process.env.LOCKING_REDIS_URL,
            },
          },
        ],
      },
    },
  ],
})
```

### Environment Variables

Make sure to add the following environment variable:

```bash
LOCKING_REDIS_URL=<YOUR_LOCKING_REDIS_URL>
```

Where `<YOUR_LOCKING_REDIS_URL>` is the URL of your Redis server, either locally or in the deployed environment.

The default Redis URL in a local environment is `redis://localhost:6379`.

### Redis Locking Module Provider Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`redisUrl\`|A string indicating the Redis connection URL.|Yes|-|
|\`redisOptions\`|An object of Redis options. Refer to the |No|-|
|\`namespace\`|A string used to prefix all locked keys with |No|\`medusa\_lock:\`|
|\`waitLockingTimeout\`|A number indicating the default timeout (in seconds) to wait while acquiring a lock. This timeout is used when no timeout is specified when executing an asynchronous job or acquiring a lock.|No|\`5\`|
|\`defaultRetryInterval\`|A number indicating the time (in milliseconds) to wait before retrying to acquire a lock.|No|\`5\`|
|\`maximumRetryInterval\`|A number indicating the maximum time (in milliseconds) to wait before retrying to acquire a lock.|No|\`200\`|

***

## Test out the Module

To test out the Redis Locking Module Provider, start the Medusa application:

```bash npm2yarn
npm run dev
```

You'll see the following message logged in the terminal:

```bash
info:    Connection to Redis in "locking-redis" provider established
```

This message indicates that the Redis Locking Module Provider has successfully connected to the Redis server.

If you set the `is_default` flag to `true` in the provider options or you only registered the Redis Locking Module Provider, the Locking Module will use it by default for all locking operations.

***

## Use Provider with Locking Module

The Redis Locking Module Provider will be the default provider if you don't register any other providers, or if you set the `is_default` flag to `true`:

```ts title="medusa-config.ts" highlights={defaultHighlights}
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/locking",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/locking-redis",
            id: "locking-redis",
            is_default: true,
            options: {
              // ...
            },
          },
        ],
      },
    },
  ],
})
```

If you use the Locking Module in your customizations, the Redis Locking Module Provider will be used by default in this case. You can also explicitly use this provider by passing its identifier `lp_locking-redis` to the Locking Module's service methods.

For example, when using the `acquire` method in a [workflow step](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md):

```ts
import { Modules } from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async ({}, { container }) => {
    const lockingModuleService = container.resolve(
      Modules.LOCKING
    )

    await lockingModuleService.acquire("prod_123", {
      provider: "lp_locking-redis",
    })
  } 
)
```


# Local Notification Module Provider

The Local Notification Module Provider simulates sending a notification, but only logs the notification's details in the terminal. This is useful for development.

***

## Register the Local Notification Module

The Local Notification Module Provider is registered by default in your application. It's configured to run on the `feed` channel.

Add the module into the `providers` array of the Notification Module:

Only one provider can be defined for a channel.

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          // ...
          {
            resolve: "@medusajs/medusa/notification-local",
            id: "local",
            options: {
              channels: ["email"],
            },
          },
        ],
      },
    },
  ],
})
```

### Local Notification Module Options

|Option|Description|
|---|---|---|
|\`channels\`|The channels this notification module is used to send notifications for. While the local notification module doesn't actually send the notification,
it's important to specify its channels to make sure it's used when a notification for that channel is created.|

***

## Send Notifications to the Admin Notification Panel

The Local Notification Module Provider can also be used to send notifications to the [Medusa Admin's notification panel](https://docs.medusajs.com/user-guide#check-notifications/index.html.md).
You can send notifications to the admin dashboard when a certain action occurs using a subscriber, a custom workflow or a workflow hook.

For example, to send an admin notification whenever an order is placed, create a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) at `src/subscribers/order-placed.ts` with the following content:

```ts title="src/subscribers/order-placed.ts" highlights={highlights} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"

export default async function orderPlacedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const notificationModuleService = container.resolve(Modules.NOTIFICATION)

  await notificationModuleService.createNotifications({
    to: "",
    channel: "feed",
    template: "admin-ui",
    data: {
      title: "New order",
      description: `A new order has been placed`,
    },
  })
}

export const config: SubscriberConfig = {
  event: "order.placed",
}
```

In this subscriber, you:

- Resolve the Notification Module's main service from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).
- Use the `createNotifications` method of the Notification Module's main service to create a notification to be sent to the admin dashboard. By specifying the `feed` channel, the Local Notification Module Provider is used to send the notification.
- The `template` property of the `createNotifications` method's parameter must be set to `admin-ui`.
- The `data` property allows you to customize the content of the admin notification. It must contain `title` and `description` properties.

### Test Sending Notification

To test this out, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, place an order. The subscriber will run, sending a notification to the Medusa Admin's notification panel.


# Notification Module

In this document, you'll learn about the Notification Module and its providers.

Cloud offers [Medusa Emails](https://docs.medusajs.com/cloud/emails/index.html.md), a managed email service that allows you to send transactional emails with zero configuration. It is built on top of the Notification Module and provides an easy way to manage email notifications in your Cloud projects with insights and deliverability features.

## What is the Notification Module?

The Notification Module exposes the functionalities to send a notification to a customer or user. For example, sending an order confirmation email. Medusa uses the Notification Module in its core commerce features for notification operations, and you an use it in your custom features as well.

***

## How to Use the Notification Module?

You can use the Notification Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

In a step of your workflow, you can resolve the Notification Module's service and use its methods to send notifications.

For example:

```ts
import { Modules } from "@medusajs/framework/utils"
import { 
  createStep,
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async ({}, { container }) => {
    const notificationModuleService = container.resolve(
      Modules.NOTIFICATION
    )

    await notificationModuleService.createNotifications({
      to: "customer@gmail.com",
      channel: "email",
      template: "product-created",
      data,
    })
  } 
)

export const workflow = createWorkflow(
  "workflow-1",
  () => {
    step1()
  }
)
```

In the example above, you create a workflow that has a step. In the step, you resolve the service of the Notification Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

Then, you use the `createNotifications` method of the Notification Module to send an email notification.

Find a full example of sending a notification in the [Send Notification guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/send-notification/index.html.md).

***

## What is a Notification Module Provider?

A Notification Module Provider implements the underlying logic of sending notification. It either integrates a third-party service or uses custom logic to send the notification.

By default, Medusa uses the [Local Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/local/index.html.md) which only simulates sending the notification by logging a message in the terminal.

Medusa provides other Notification Modules that actually send notifications, such as the [SendGrid Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/send-notification/index.html.md). You can also [Create a Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md).

- [Local](https://docs.medusajs.com/infrastructure-modules/notification/local/index.html.md)
- [SendGrid](https://docs.medusajs.com/infrastructure-modules/notification/sendgrid/index.html.md)
- [Mailchimp](https://docs.medusajs.com/integrations/guides/mailchimp/index.html.md)
- [Resend](https://docs.medusajs.com/integrations/guides/resend/index.html.md)
- [Slack](https://docs.medusajs.com/integrations/guides/slack/index.html.md)
- [Twilio SMS](https://docs.medusajs.com/how-to-tutorials/tutorials/phone-auth#step-3-integrate-twilio-sms/index.html.md)

***

## Notification Module Provider Channels

When you send a notification, you specify the channel to send it through, such as `email` or `sms`.

You register providers of the Notification Module in `medusa-config.ts`. For each provider, you pass a `channels` option specifying which channels the provider can be used in. Only one provider can be setup for each channel.

For example:

```ts title="medusa-config.ts" highlights={[["19"]]}
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = {
  // ...
  modules: [
    // ...
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          // ...
          {
            resolve: "@medusajs/medusa/notification-local",
            id: "notification",
            options: {
              channels: ["email"],
            },
          },
        ],
      },
    },
  ],
}
```

The `channels` option is an array of strings indicating the channels this provider is used for.


# Send Notification with the Notification Module

In this guide, you'll learn about the different ways to send notifications using the Notification Module.

## Using the Create Method

In your resource, such as a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md), resolve the Notification Module's main service and use its `create` method:

```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"
import { INotificationModuleService } from "@medusajs/framework/types"

export default async function productCreateHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const notificationModuleService: INotificationModuleService =
    container.resolve(Modules.NOTIFICATION)

  await notificationModuleService.createNotifications({
    to: "user@gmail.com",
    channel: "email",
    template: "product-created",
    data,
  })
}

export const config: SubscriberConfig = {
  event: "product.created",
}
```

The `create` method accepts an object or an array of objects having the following properties:

- to: (\`string\`) The destination to send the notification to. When sending an email, it'll be the email address. When sending an SMS, it'll be the phone number.
- channel: (\`string\`) The channel to send the notification through. For example, \`email\` or \`sms\`. The module provider defined for that channel will be used to send the notification.
- template: (\`string\`) The ID of the template used for the notification. This is useful for providers like SendGrid, where you define templates within SendGrid and use their IDs here.
- data: (\`Record\<string, unknown>\`) The data to pass along to the template, if necessary.

For a full list of properties accepted, refer to [this guide](https://docs.medusajs.com/references/notification-provider-module#create/index.html.md).

***

## Using the sendNotificationsStep

If you want to send a notification as part of a workflow, You can use the [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) in your workflow.

For example:

```ts title="src/workflows/send-email.ts"
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
import { 
  sendNotificationsStep, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"

type WorkflowInput = {
  id: string
}

export const sendEmailWorkflow = createWorkflow(
  "send-email-workflow",
  ({ id }: WorkflowInput) => {
    const { data: products } = useQueryGraphStep({
      entity: "product",
      fields: [
        "*",
        "variants.*",
      ],
      filters: {
        id,
      },
    })

    sendNotificationsStep({
      to: "user@gmail.com",
      channel: "email",
      template: "product-created",
      data: {
        product_title: product[0].title,
        product_image: product[0].images[0]?.url,
      },
    })
  }
)
```

For a full list of input properties accepted, refer to the [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) reference.

You can then execute this workflow in a subscriber, API route, or scheduled job.

For example, you can execute it when a product is created:

```ts title="src/subscribers/product-created.ts"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { sendEmailWorkflow } from "../workflows/send-email"

export default async function productCreateHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await sendEmailWorkflow(container).run({
    input: {
      id: data.id,
    },
  })
}

export const config: SubscriberConfig = {
  event: "product.created",
}
```


# SendGrid Notification Module Provider

The SendGrid Notification Module Provider integrates [SendGrid](https://sendgrid.com) to send emails to users and customers.

Cloud offers [Medusa Emails](https://docs.medusajs.com/cloud/emails/index.html.md), a managed email service that allows you to send transactional emails with zero configuration. It is built on top of the Notification Module and provides an easy way to manage email notifications in your Cloud projects with insights and deliverability features.

## Register the SendGrid Notification Module

### Prerequisites

- [SendGrid account](https://signup.sendgrid.com)
- [Setup SendGrid single sender](https://docs.sendgrid.com/ui/sending-email/sender-verification)
- [SendGrid API Key](https://docs.sendgrid.com/ui/account-and-settings/api-keys)

Add the module into the `providers` array of the Notification Module:

Only one provider can be defined for a channel.

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          // ...
          {
            resolve: "@medusajs/medusa/notification-sendgrid",
            id: "sendgrid",
            options: {
              channels: ["email"],
              api_key: process.env.SENDGRID_API_KEY,
              from: process.env.SENDGRID_FROM,
            },
          },
        ],
      },
    },
  ],
})
```

### Environment Variables

Make sure to add the following environment variables:

```bash
SENDGRID_API_KEY=<YOUR_SENDGRID_API_KEY>
SENDGRID_FROM=<YOUR_SENDGRID_FROM>
```

### SendGrid Notification Module Options

|Option|Description|
|---|---|---|
||The channels this notification module is used to send notifications for.
Only one provider can be defined for a channel.|
|
|
|
|

## SendGrid Templates

When you send a notification, you must specify the ID of the template to use in SendGrid.

Refer to [this SendGrid documentation guide](https://docs.sendgrid.com/ui/sending-email/how-to-send-an-email-with-dynamic-templates) on how to create templates for your different email types.

***

## Test out the Module

To test the module out, you'll listen to the `product.created` event and send an email when a product is created.

Create a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) at `src/subscribers/product-created.ts` with the following content:

```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"

export default async function productCreateHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const notificationModuleService = container.resolve(Modules.NOTIFICATION)
  const query = container.resolve("query")

  const { data: [product] } = await query.graph({
    entity: "product",
    fields: ["*"],
    filters: {
      id: data.id,
    },
  })

  await notificationModuleService.createNotifications({
    to: "test@gmail.com",
    channel: "email",
    template: "product-created",
    data: {
      product_title: product.title,
      product_image: product.images[0]?.url,
    },
  })
}

export const config: SubscriberConfig = {
  event: "product.created",
}
```

In this subscriber, you:

- Resolve the Notification Module's main service and [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).
- Retrieve the product's details using Query to pass them to the template in SendGrid.
- Use the `createNotifications` method of the Notification Module's main service to create a notification to be sent to the specified email. By specifying the `email` channel, the SendGrid Notification Module Provider is used to send the notification.
- The `template` property of the `createNotifications` method's parameter specifies the ID of the template defined in SendGrid.
- The `data` property allows you to pass data to the template in SendGrid. For example, the product's title and image.

Then, start the Medusa application:

```bash npm2yarn
npm run dev
```

And create a product either using the [API route](https://docs.medusajs.com/api/admin#products_postproducts) or the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/index.html.md). This runs the subscriber and sends an email using SendGrid.

### Other Events to Handle

Medusa emits other events that you can handle to send notifications using the SendGrid Notification Module Provider, such as `order.placed` when an order is placed.

Refer to the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for a complete list of events emitted by Medusa.

### Sending Emails with SendGrid in Workflows

You can also send an email using SendGrid in any [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). This allows you to send emails within your custom flows.

You can use the [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) in your workflow to send an email using SendGrid.

For example:

```ts title="src/workflows/send-email.ts"
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
import { 
  sendNotificationsStep, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"

type WorkflowInput = {
  id: string
}

export const sendEmailWorkflow = createWorkflow(
  "send-email-workflow",
  ({ id }: WorkflowInput) => {
    const { data: products } = useQueryGraphStep({
      entity: "product",
      fields: [
        "*",
        "variants.*",
      ],
      filters: {
        id,
      },
    })

    sendNotificationsStep({
      to: "test@gmail.com",
      channel: "email",
      template: "product-created",
      data: {
        product_title: product[0].title,
        product_image: product[0].images[0]?.url,
      },
    })
  }
)
```

This workflow works similarly to the subscriber. It retrieves the product's details using Query and sends an email using SendGrid (by specifying the `email` channel) to the `test@gmail.com` email.

You can also execute this workflow in a subscriber. For example, you can execute it when a product is created:

```ts title="src/subscribers/product-created.ts"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"
import { sendEmailWorkflow } from "../workflows/send-email"

export default async function productCreateHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await sendEmailWorkflow(container).run({
    input: {
      id: data.id,
    },
  })
}

export const config: SubscriberConfig = {
  event: "product.created",
}
```

This subscriber will run every time a product is created, and it will execute the `sendEmailWorkflow` to send an email using SendGrid.


# Infrastructure Modules

Medusa's architectural functionalities, such as emitting and subscribing to events or caching data, are all implemented in Infrastructure Modules. An Infrastructure Module is a package that can be installed and used in any Medusa application. These modules allow you to choose and integrate custom services for architectural purposes.

For example, you can use our [Redis Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/redis/index.html.md) to handle event functionalities, or create a custom module that implements these functionalities with Memcached. Learn more in [the Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

This section of the documentation showcases Medusa's Infrastructure Modules, how they work, and how to use them in your Medusa application.

## Analytics Module

The Analytics Module is available starting [Medusa v2.8.3](https://github.com/medusajs/medusa/releases/tag/v2.8.3).

The Analytics Module exposes functionalities to track and analyze user interactions and system events. For example, tracking cart updates or completed orders. Learn more in the [Analytics Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/analytics/index.html.md).

{/* The Analytics Module has module providers that implement the underlying logic of integrating third-party services for tracking analytics. The following Analytics Module Providers are provided by Medusa. You can also create a custom provider as explained in the [Create Analytics Module Provider guide](/references/analytics/provider). */}

- [Local](https://docs.medusajs.com/infrastructure-modules/analytics/local/index.html.md)
- [PostHog](https://docs.medusajs.com/infrastructure-modules/analytics/posthog/index.html.md)

## Caching Module

The Caching Module provides functionality to cache data in your Medusa application, improving performance and reducing latency for frequently accessed data.

The following Caching modules are provided by Medusa. You can also create a custom Caching Module Provider as explained in the [Create Caching Module Provider guide](#).

The Caching Module is available starting [Medusa v2.11.0](https://github.com/medusajs/medusa/releases/tag/v2.11.0). It replaces the deprecated [Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/index.html.md).

- [Redis](https://docs.medusajs.com/infrastructure-modules/caching/providers/redis/index.html.md)
- [Memcached](#)

***

## Event Module

An Event Module implements the underlying publish/subscribe system that handles queueing events, emitting them, and executing their subscribers.  Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/index.html.md).

The following Event modules are provided by Medusa. You can also create your own event module as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/create/index.html.md).

- [Local](https://docs.medusajs.com/infrastructure-modules/event/local/index.html.md)
- [Redis](https://docs.medusajs.com/infrastructure-modules/event/redis/index.html.md)

***

## File Module

The File Module handles file upload and storage of assets, such as product images. Refer to the [File Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/index.html.md) to learn more about it.

The File Module has module providers that implement the underlying logic of handling uploads and downloads of assets, such as integrating third-party services. The following File Module Providers are provided by Medusa. You can also create a custom provider as explained in the [Create File Module Provider guide](https://docs.medusajs.com/references/file-provider-module/index.html.md).

- [Local](https://docs.medusajs.com/infrastructure-modules/file/local/index.html.md)
- [AWS S3 (and Compatible APIs)](https://docs.medusajs.com/infrastructure-modules/file/s3/index.html.md)

***

## Locking Module

The Locking Module manages access to shared resources by multiple processes or threads. It prevents conflicts between processes and ensures data consistency. Refer to the [Locking Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/locking/index.html.md) to learn more about it.

The Locking Module uses module providers that implement the underlying logic of the locking mechanism. The following Locking Module Providers are provided by Medusa. You can also create a custom provider as explained in the [Create Locking Module Provider guide](https://docs.medusajs.com/references/locking-module-provider/index.html.md).

- [Redis](https://docs.medusajs.com/infrastructure-modules/locking/redis/index.html.md)
- [PostgreSQL](https://docs.medusajs.com/infrastructure-modules/locking/postgres/index.html.md)

***

## Notification Module

The Notification Module handles sending notifications to users or customers, such as reset password instructions or newsletters. Refer to the [Notification Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) to learn more about it.

The Notification Module has module providers that implement the underlying logic of sending notifications, typically through integrating a third-party service. The following modules are provided by Medusa. You can also create a custom provider as explained in the [Create Notification Module Provider guide](https://docs.medusajs.com/references/notification-provider-module/index.html.md).

- [Local](https://docs.medusajs.com/infrastructure-modules/notification/local/index.html.md)
- [SendGrid](https://docs.medusajs.com/infrastructure-modules/notification/sendgrid/index.html.md)

### Notification Module Provider Guides

- [Send Notification](https://docs.medusajs.com/infrastructure-modules/notification/send-notification/index.html.md)
- [Create Notification Provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md)
- [Resend](https://docs.medusajs.com/integrations/guides/resend/index.html.md)

***

## Workflow Engine Module

A Workflow Engine Module handles tracking and recording the transactions and statuses of workflows and their steps. Learn more about it in the [Workflow Engine Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/index.html.md).

The following Workflow Engine modules are provided by Medusa.

- [In-Memory](https://docs.medusajs.com/infrastructure-modules/workflow-engine/in-memory/index.html.md)
- [Redis](https://docs.medusajs.com/infrastructure-modules/workflow-engine/redis/index.html.md)


# How to Use the Workflow Engine Module

In this document, you’ll learn about the different methods in the Workflow Engine Module's service and how to use them.

***

## Resolve Workflow Engine Module's Service

In your workflow's step, you can resolve the Workflow Engine Module's service from the Medusa container:

```ts
import { Modules } from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async ({}, { container }) => {
    const workflowEngineModuleService = container.resolve(
      Modules.WORKFLOW_ENGINE
    )
    
    // TODO use workflowEngineModuleService
  } 
)
```

This will resolve the service of the configured Workflow Engine Module, which is the [In-Memory Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/in-memory/index.html.md) by default.

You can then use the Workflow Engine Module's service's methods in the step. The rest of this guide details these methods.

***

## retryStep

This method retries a step that has temporary failed, such as a step that has `autoRetry` set to `false` or when the machine running the Medusa application shuts down. Learn more about it in the [Retry Failed Steps](https://docs.medusajs.com/docs/learn/fundamentals/workflows/retry-failed-steps/index.html.md) guide.

This method is available since [Medusa v2.10.2](https://github.com/medusajs/medusa/releases/tag/v2.10.2).

### Example

```ts
// other imports...
import {
  TransactionHandlerType,
} from "@medusajs/framework/utils"

workflowEngine.retryStep({
  idempotencyKey: {
    action: TransactionHandlerType.INVOKE,
    transactionId,
    stepId: "step-1",
    workflowId: "hello-world",
  },
})
```

### Parameters

- idempotencyKey: (\`object\`) The details of the step to retry.

  - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`.

  - transactionId: (\`string\`) The ID of the workflow execution's transaction.

  - stepId: (\`string\`) The ID of the step to change its status. This is the first parameter passed to \`createStep\` when creating the step.

  - workflowId: (\`string\`) The ID of the workflow. This is the first parameter passed to \`createWorkflow\` when creating the workflow.
- options: (\`object\`) Options to pass to the step.

  - container: (\`Container\`) An instance of the Medusa container.

***

## setStepSuccess

This method sets an async step in a currently-executing [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md) as successful. The workflow will then continue to the next step.

### Example

```ts
// other imports...
import {
  TransactionHandlerType,
} from "@medusajs/framework/utils"

await workflowEngineModuleService.setStepSuccess({
  idempotencyKey: {
    action: TransactionHandlerType.INVOKE,
    transactionId,
    stepId: "step-2",
    workflowId: "hello-world",
  },
  stepResponse: new StepResponse("Done!"),
})
```

### Parameters

- idempotencyKey: (\`object\`) The details of the step to set as successful.

  - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`.

  - transactionId: (\`string\`) The ID of the workflow execution's transaction.

  - stepId: (\`string\`) The ID of the step to change its status. This is the first parameter passed to \`createStep\` when creating the step.

  - workflowId: (\`string\`) The ID of the workflow. This is the first parameter passed to \`createWorkflow\` when creating the workflow.
- stepResponse: (\`StepResponse\`) Set the response of the step. This is similar to the response you return in a step's definition, but since the async step doesn't have a response, you set its response when changing its status.
- options: (\`object\`) Options to pass to the step.

  - container: (\`Container\`) An instance of the Medusa container.

***

## setStepFailure

This method sets an async step in a currently-executing [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md) as failed. The workflow will then stop executing and the compensation functions of the workflow's steps will be executed.

### Example

```ts
// other imports...
import {
  TransactionHandlerType,
} from "@medusajs/framework/utils"

await workflowEngineModuleService.setStepFailure({
  idempotencyKey: {
    action: TransactionHandlerType.INVOKE,
    transactionId,
    stepId: "step-2",
    workflowId: "hello-world",
  },
  stepResponse: new StepResponse("Failed!"),
})
```

### Parameters

- idempotencyKey: (\`object\`) The details of the step to set as failed.

  - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`.

  - transactionId: (\`string\`) The ID of the workflow execution's transaction.

  - stepId: (\`string\`) The ID of the step to change its status. This is the first parameter passed to \`createStep\` when creating the step.

  - workflowId: (\`string\`) The ID of the workflow. This is the first parameter passed to \`createWorkflow\` when creating the workflow.
- stepResponse: (\`StepResponse\`) Set the response of the step. This is similar to the response you return in a step's definition, but since the async step doesn't have a response, you set its response when changing its status.
- options: (\`object\`) Options to pass to the step.

  - container: (\`Container\`) An instance of the Medusa container.

***

## subscribe

This method subscribes to a workflow's events. You can use this method to listen to a [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md)'s events and retrieve its result once it's done executing.

Refer to the [Long-Running Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow#access-long-running-workflow-status-and-result/index.html.md) documentation to learn more.

### Example

```ts
const { transaction } = await helloWorldWorkflow(container).run()

const subscriptionOptions = {
  workflowId: "hello-world",
  transactionId: transaction.transactionId,
  subscriberId: "hello-world-subscriber",
}

await workflowEngineModuleService.subscribe({
  ...subscriptionOptions,
  subscriber: async (data) => {
    if (data.eventType === "onFinish") {
      console.log("Finished execution", data.result)
      // unsubscribe
      await workflowEngineModuleService.unsubscribe({
        ...subscriptionOptions,
        subscriberOrId: subscriptionOptions.subscriberId,
      })
    } else if (data.eventType === "onStepFailure") {
      console.log("Workflow failed", data.step)
    }
  },
})
```

### Parameters

- subscriptionOptions: (\`object\`) The options for the subscription.

  - workflowId: (\`string\`) The ID of the workflow to subscribe to. This is the first parameter passed to \`createWorkflow\` when creating the workflow.

  - transactionId: (\`string\`) The ID of the workflow execution's transaction. This is returned when you execute a workflow.

  - subscriberId: (\`string\`) A unique ID for the subscriber. It's used to unsubscribe from the workflow's events.

  - subscriber: (\`(data: WorkflowEvent) => void\`) The subscriber function that will be called when the workflow emits an event.

***

## unsubscribe

This method unsubscribes from a workflow's events. You can use this method to stop listening to a [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md)'s events after you've received the result.

### Example

```ts
await workflowEngineModuleService.unsubscribe({
  workflowId: "hello-world",
  transactionId: "transaction-id",
  subscriberOrId: "hello-world-subscriber",
})
```

### Parameters

- workflowId: (\`string\`) The ID of the workflow to unsubscribe from. This is the first parameter passed to \`createWorkflow\` when creating the workflow.
- transactionId: (\`string\`) The ID of the workflow execution's transaction. This is returned when you execute a workflow.
- subscriberOrId: (\`string\`) The subscriber ID or the subscriber function to unsubscribe from the workflow's events.


# In-Memory Workflow Engine Module

The In-Memory Workflow Engine Module uses a plain JavaScript Map object to store the workflow executions.

This module is helpful for development or when you’re testing out Medusa, but it’s not recommended to be used in production.

For production, it’s recommended to use modules like [Redis Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/redis/index.html.md).

***

## Register the In-Memory Workflow Engine Module

The In-Memory Workflow Engine Module is registered by default in your application.

Add the module into the `modules` property of the exported object in `medusa-config.ts`:

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/workflow-engine-inmemory",
    },
  ],
})
```


# Workflow Engine Module

In this document, you'll learn what a Workflow Engine Module is and how to use it in your Medusa application.

## What is a Workflow Engine Module?

A Workflow Engine Module handles tracking and recording the transactions and statuses of workflows and their steps. It can use custom mechanism or integrate a third-party service.

### Default Workflow Engine Module

Medusa uses the [In-Memory Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/in-memory/index.html.md) by default. For production purposes, it's recommended to use the [Redis Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/redis/index.html.md) instead.

***

## How to Use the Workflow Engine Module?

You can use the registered Workflow Engine Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

In a step of your workflow, you can resolve the Workflow Engine Module's service and use its methods to track and record the transactions and statuses of workflows and their steps.

For example:

```ts
import { Modules } from "@medusajs/framework/utils"
import { 
  createStep,
  createWorkflow,
  StepResponse,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async ({}, { container }) => {
    const workflowEngineService = container.resolve(
      Modules.WORKFLOW_ENGINE
    )

    const [workflowExecution] = await workflowEngineService.listWorkflowExecutions({
      transaction_id: transaction_id,
    })

    return new StepResponse(workflowExecution)
  } 
)

export const workflow = createWorkflow(
  "workflow-1",
  () => {
    const workflowExecution = step1()

    return new WorkflowResponse(workflowExecution)
  }
)
```

In the example above, you create a workflow that has a step. In the step, you resolve the service of the Workflow Engine Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

Then, you use the `listWorkflowExecutions` method of the Workflow Engine Module to list the workflow executions with the transaction ID `transaction_id`. The workflow execution is then returned as a response from the step and the workflow.

***

## List of Workflow Engine Modules

Medusa provides the following Workflow Engine Modules.

- [In-Memory](https://docs.medusajs.com/infrastructure-modules/workflow-engine/in-memory/index.html.md)
- [Redis](https://docs.medusajs.com/infrastructure-modules/workflow-engine/redis/index.html.md)


# Redis Workflow Engine Module

The Redis Workflow Engine Module uses Redis to track workflow executions and handle their subscribers. In production, it's recommended to use this module.

Our Cloud offering automatically provisions a Redis instance and configures the Redis Workflow Engine Module for you. Learn more in the [Redis](https://docs.medusajs.com/cloud/redis/index.html.md) Cloud documentation.

***

## Register the Redis Workflow Engine Module

### Prerequisites

- [Redis installed and Redis server running](https://redis.io/docs/getting-started/installation/)

Add the module into the `modules` property of the exported object in `medusa-config.ts`:

```ts title="medusa-config.ts" highlights={highlights}
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/workflow-engine-redis",
      options: {
        redis: {
          redisUrl: process.env.WE_REDIS_URL,
          // ...other Redis options
        },
      },
    },
  ],
})
```

### Environment Variables

Make sure to add the following environment variables:

```bash
WE_REDIS_URL=<YOUR_REDIS_URL>
```

### Redis Workflow Engine Module Options

|Option|Description|Required|
|---|---|---|---|---|
|\`url\`|A string indicating the Redis connection URL.|No. If not provided, you must provide the |
|\`redisUrl\`|A string indicating the Redis connection URL.|No. If not provided, you must provide the |
|\`pubsub\`|A connection object having the following properties:|No. If not provided, you must provide the |
|\`queueName\`|The name of the queue used to keep track of retries and timeouts for workflow executions.|No|
|\`jobQueueName\`|The name of the queue used to keep track of retries and timeouts for scheduled jobs.|No|
|\`options\`|An object of Redis options. Refer to the |No|
|\`redisOptions\`|An object of Redis options. Refer to the |No|
|\`queueOptions\`|An object of options to pass to all BullMQ instances. Refer to |No|
|\`workerOptions\`|An object of options to pass to all BullMQ worker instances. Refer to |No|
|\`mainQueueOptions\`|An object of options to pass to the main BullMQ queue instance for workflows. Refer to |No|
|\`mainWorkerOptions\`|An object of options to pass to the main BullMQ worker instance for workflows. Refer to |No|
|\`jobQueueOptions\`|An object of options to pass to the main BullMQ workflow queue instance for scheduled jobs. Refer to |No|
|\`jobWorkerOptions\`|An object of options to pass to the main BullMQ workflow worker instance for scheduled jobs. Refer to |No|
|\`cleanerQueueOptions\`|An object of options to pass to the BullMQ cleaner queue instance. Refer to |No|
|\`cleanerWorkerOptions\`|An object of options to pass to the BullMQ cleaner worker instance. Refer to |No|

## Test the Module

To test the module, start the Medusa application:

```bash npm2yarn
npm run dev
```

You'll see the following message in the terminal's logs:

```bash noCopy noReport
Connection to Redis in module 'workflow-engine-redis' established
```


## Workflows

- [createApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/createApiKeysWorkflow/index.html.md)
- [deleteApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteApiKeysWorkflow/index.html.md)
- [linkSalesChannelsToApiKeyWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToApiKeyWorkflow/index.html.md)
- [revokeApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/revokeApiKeysWorkflow/index.html.md)
- [updateApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateApiKeysWorkflow/index.html.md)
- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md)
- [setAuthAppMetadataWorkflow](https://docs.medusajs.com/references/medusa-workflows/setAuthAppMetadataWorkflow/index.html.md)
- [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md)
- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md)
- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md)
- [confirmVariantInventoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmVariantInventoryWorkflow/index.html.md)
- [createCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartCreditLinesWorkflow/index.html.md)
- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md)
- [createPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentCollectionForCartWorkflow/index.html.md)
- [deleteCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCartCreditLinesWorkflow/index.html.md)
- [listShippingOptionsForCartWithPricingWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow/index.html.md)
- [listShippingOptionsForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWorkflow/index.html.md)
- [refreshCartItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartItemsWorkflow/index.html.md)
- [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/index.html.md)
- [refreshPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshPaymentCollectionForCartWorkflow/index.html.md)
- [refundPaymentAndRecreatePaymentSessionWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentAndRecreatePaymentSessionWorkflow/index.html.md)
- [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md)
- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartPromotionsWorkflow/index.html.md)
- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md)
- [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/index.html.md)
- [updateTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxLinesWorkflow/index.html.md)
- [validateExistingPaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/validateExistingPaymentCollectionStep/index.html.md)
- [batchLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinksWorkflow/index.html.md)
- [createLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLinksWorkflow/index.html.md)
- [dismissLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissLinksWorkflow/index.html.md)
- [updateLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLinksWorkflow/index.html.md)
- [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md)
- [createCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAddressesWorkflow/index.html.md)
- [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/index.html.md)
- [deleteCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerAddressesWorkflow/index.html.md)
- [deleteCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomersWorkflow/index.html.md)
- [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/index.html.md)
- [updateCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerAddressesWorkflow/index.html.md)
- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md)
- [createCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerGroupsWorkflow/index.html.md)
- [deleteCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerGroupsWorkflow/index.html.md)
- [linkCustomerGroupsToCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomerGroupsToCustomerWorkflow/index.html.md)
- [linkCustomersToCustomerGroupWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomersToCustomerGroupWorkflow/index.html.md)
- [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/index.html.md)
- [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/index.html.md)
- [addDraftOrderItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderItemsWorkflow/index.html.md)
- [addDraftOrderPromotionWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderPromotionWorkflow/index.html.md)
- [addDraftOrderShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderShippingMethodsWorkflow/index.html.md)
- [beginDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginDraftOrderEditWorkflow/index.html.md)
- [cancelDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelDraftOrderEditWorkflow/index.html.md)
- [computeDraftOrderAdjustmentsWorkflow](https://docs.medusajs.com/references/medusa-workflows/computeDraftOrderAdjustmentsWorkflow/index.html.md)
- [confirmDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmDraftOrderEditWorkflow/index.html.md)
- [convertDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderStep/index.html.md)
- [convertDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderWorkflow/index.html.md)
- [deleteDraftOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteDraftOrdersWorkflow/index.html.md)
- [removeDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderActionItemWorkflow/index.html.md)
- [removeDraftOrderActionShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderActionShippingMethodWorkflow/index.html.md)
- [removeDraftOrderPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderPromotionsWorkflow/index.html.md)
- [removeDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderShippingMethodWorkflow/index.html.md)
- [requestDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestDraftOrderEditWorkflow/index.html.md)
- [updateDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionItemWorkflow/index.html.md)
- [updateDraftOrderActionShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionShippingMethodWorkflow/index.html.md)
- [updateDraftOrderItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderItemWorkflow/index.html.md)
- [updateDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderShippingMethodWorkflow/index.html.md)
- [updateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderStep/index.html.md)
- [updateDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderWorkflow/index.html.md)
- [deleteFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFilesWorkflow/index.html.md)
- [uploadFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/uploadFilesWorkflow/index.html.md)
- [batchShippingOptionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchShippingOptionRulesWorkflow/index.html.md)
- [calculateShippingOptionsPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/calculateShippingOptionsPricesWorkflow/index.html.md)
- [cancelFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelFulfillmentWorkflow/index.html.md)
- [createFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentWorkflow/index.html.md)
- [createReturnFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnFulfillmentWorkflow/index.html.md)
- [createServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createServiceZonesWorkflow/index.html.md)
- [createShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShipmentWorkflow/index.html.md)
- [createShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingOptionsWorkflow/index.html.md)
- [createShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingProfilesWorkflow/index.html.md)
- [deleteFulfillmentSetsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFulfillmentSetsWorkflow/index.html.md)
- [deleteServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteServiceZonesWorkflow/index.html.md)
- [deleteShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingOptionsWorkflow/index.html.md)
- [markFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markFulfillmentAsDeliveredWorkflow/index.html.md)
- [updateFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateFulfillmentWorkflow/index.html.md)
- [updateServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateServiceZonesWorkflow/index.html.md)
- [updateShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingOptionsWorkflow/index.html.md)
- [updateShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingProfilesWorkflow/index.html.md)
- [validateFulfillmentDeliverabilityStep](https://docs.medusajs.com/references/medusa-workflows/validateFulfillmentDeliverabilityStep/index.html.md)
- [batchInventoryItemLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchInventoryItemLevelsWorkflow/index.html.md)
- [bulkCreateDeleteLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/bulkCreateDeleteLevelsWorkflow/index.html.md)
- [createInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryItemsWorkflow/index.html.md)
- [createInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryLevelsWorkflow/index.html.md)
- [deleteInventoryItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryItemWorkflow/index.html.md)
- [deleteInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryLevelsWorkflow/index.html.md)
- [updateInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryItemsWorkflow/index.html.md)
- [updateInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryLevelsWorkflow/index.html.md)
- [validateInventoryLevelsDelete](https://docs.medusajs.com/references/medusa-workflows/validateInventoryLevelsDelete/index.html.md)
- [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md)
- [createInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInvitesWorkflow/index.html.md)
- [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/index.html.md)
- [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/index.html.md)
- [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/index.html.md)
- [acceptOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferValidationStep/index.html.md)
- [acceptOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferWorkflow/index.html.md)
- [addOrderLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrderLineItemsWorkflow/index.html.md)
- [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md)
- [beginClaimOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderValidationStep/index.html.md)
- [beginClaimOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderWorkflow/index.html.md)
- [beginExchangeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginExchangeOrderWorkflow/index.html.md)
- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md)
- [beginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditValidationStep/index.html.md)
- [beginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderExchangeValidationStep/index.html.md)
- [beginReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnValidationStep/index.html.md)
- [beginReceiveReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnWorkflow/index.html.md)
- [beginReturnOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderValidationStep/index.html.md)
- [beginReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderWorkflow/index.html.md)
- [cancelBeginOrderClaimValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimValidationStep/index.html.md)
- [cancelBeginOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimWorkflow/index.html.md)
- [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/index.html.md)
- [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/index.html.md)
- [cancelBeginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeValidationStep/index.html.md)
- [cancelBeginOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeWorkflow/index.html.md)
- [cancelClaimValidateOrderStep](https://docs.medusajs.com/references/medusa-workflows/cancelClaimValidateOrderStep/index.html.md)
- [cancelExchangeValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelExchangeValidateOrder/index.html.md)
- [cancelOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderChangeWorkflow/index.html.md)
- [cancelOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderClaimWorkflow/index.html.md)
- [cancelOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderExchangeWorkflow/index.html.md)
- [cancelOrderFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentValidateOrder/index.html.md)
- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md)
- [cancelOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderTransferRequestWorkflow/index.html.md)
- [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/index.html.md)
- [cancelReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelReceiveReturnValidationStep/index.html.md)
- [cancelRequestReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelRequestReturnValidationStep/index.html.md)
- [cancelReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnReceiveWorkflow/index.html.md)
- [cancelReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnRequestWorkflow/index.html.md)
- [cancelReturnValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelReturnValidateOrder/index.html.md)
- [cancelReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnWorkflow/index.html.md)
- [cancelTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelTransferOrderRequestValidationStep/index.html.md)
- [cancelValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelValidateOrder/index.html.md)
- [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md)
- [computeAdjustmentsForPreviewWorkflow](https://docs.medusajs.com/references/medusa-workflows/computeAdjustmentsForPreviewWorkflow/index.html.md)
- [confirmClaimRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestValidationStep/index.html.md)
- [confirmClaimRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestWorkflow/index.html.md)
- [confirmExchangeRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestValidationStep/index.html.md)
- [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md)
- [confirmOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestValidationStep/index.html.md)
- [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md)
- [confirmReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReceiveReturnValidationStep/index.html.md)
- [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md)
- [confirmReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestValidationStep/index.html.md)
- [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md)
- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md)
- [createClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodValidationStep/index.html.md)
- [createClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodWorkflow/index.html.md)
- [createCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/createCompleteReturnValidationStep/index.html.md)
- [createExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodValidationStep/index.html.md)
- [createExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodWorkflow/index.html.md)
- [createFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentValidateOrder/index.html.md)
- [createOrUpdateOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrUpdateOrderPaymentCollectionWorkflow/index.html.md)
- [createOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeActionsWorkflow/index.html.md)
- [createOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeWorkflow/index.html.md)
- [createOrderCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderCreditLinesWorkflow/index.html.md)
- [createOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodValidationStep/index.html.md)
- [createOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodWorkflow/index.html.md)
- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md)
- [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/index.html.md)
- [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/index.html.md)
- [createOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderWorkflow/index.html.md)
- [createOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrdersWorkflow/index.html.md)
- [createReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodValidationStep/index.html.md)
- [createReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodWorkflow/index.html.md)
- [createShipmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createShipmentValidateOrder/index.html.md)
- [declineOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderChangeWorkflow/index.html.md)
- [declineOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderTransferRequestWorkflow/index.html.md)
- [declineTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/declineTransferOrderRequestValidationStep/index.html.md)
- [deleteOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeActionsWorkflow/index.html.md)
- [deleteOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeWorkflow/index.html.md)
- [deleteOrderPaymentCollections](https://docs.medusajs.com/references/medusa-workflows/deleteOrderPaymentCollections/index.html.md)
- [dismissItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestValidationStep/index.html.md)
- [dismissItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestWorkflow/index.html.md)
- [exchangeAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeAddNewItemValidationStep/index.html.md)
- [exchangeRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeRequestItemReturnValidationStep/index.html.md)
- [exportOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/exportOrdersWorkflow/index.html.md)
- [fetchShippingOptionForOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/fetchShippingOptionForOrderWorkflow/index.html.md)
- [getOrderDetailWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrderDetailWorkflow/index.html.md)
- [getOrdersListWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrdersListWorkflow/index.html.md)
- [listShippingOptionsForOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForOrderWorkflow/index.html.md)
- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md)
- [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/index.html.md)
- [maybeRefreshShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/maybeRefreshShippingMethodsWorkflow/index.html.md)
- [onCarryPromotionsFlagSet](https://docs.medusajs.com/references/medusa-workflows/onCarryPromotionsFlagSet/index.html.md)
- [orderClaimAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemValidationStep/index.html.md)
- [orderClaimAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemWorkflow/index.html.md)
- [orderClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemValidationStep/index.html.md)
- [orderClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemWorkflow/index.html.md)
- [orderClaimRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnValidationStep/index.html.md)
- [orderClaimRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnWorkflow/index.html.md)
- [orderEditAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemValidationStep/index.html.md)
- [orderEditAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemWorkflow/index.html.md)
- [orderEditUpdateItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityValidationStep/index.html.md)
- [orderEditUpdateItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityWorkflow/index.html.md)
- [orderExchangeAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeAddNewItemWorkflow/index.html.md)
- [orderExchangeRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeRequestItemReturnWorkflow/index.html.md)
- [orderFulfillmentDeliverablilityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderFulfillmentDeliverablilityValidationStep/index.html.md)
- [receiveAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveAndCompleteReturnOrderWorkflow/index.html.md)
- [receiveCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveCompleteReturnValidationStep/index.html.md)
- [receiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestValidationStep/index.html.md)
- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md)
- [removeAddItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeAddItemClaimActionWorkflow/index.html.md)
- [removeClaimAddItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimAddItemActionValidationStep/index.html.md)
- [removeClaimItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimItemActionValidationStep/index.html.md)
- [removeClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodValidationStep/index.html.md)
- [removeClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodWorkflow/index.html.md)
- [removeExchangeItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeItemActionValidationStep/index.html.md)
- [removeExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodValidationStep/index.html.md)
- [removeExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodWorkflow/index.html.md)
- [removeItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemClaimActionWorkflow/index.html.md)
- [removeItemExchangeActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemExchangeActionWorkflow/index.html.md)
- [removeItemOrderEditActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemOrderEditActionWorkflow/index.html.md)
- [removeItemReceiveReturnActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionValidationStep/index.html.md)
- [removeItemReceiveReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionWorkflow/index.html.md)
- [removeItemReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReturnActionWorkflow/index.html.md)
- [removeOrderEditItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditItemActionValidationStep/index.html.md)
- [removeOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodValidationStep/index.html.md)
- [removeOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodWorkflow/index.html.md)
- [removeReturnItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnItemActionValidationStep/index.html.md)
- [removeReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodValidationStep/index.html.md)
- [removeReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodWorkflow/index.html.md)
- [requestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnValidationStep/index.html.md)
- [requestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnWorkflow/index.html.md)
- [requestOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestValidationStep/index.html.md)
- [requestOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestWorkflow/index.html.md)
- [requestOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferValidationStep/index.html.md)
- [requestOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferWorkflow/index.html.md)
- [throwUnlessPaymentCollectionNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessPaymentCollectionNotPaid/index.html.md)
- [throwUnlessStatusIsNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessStatusIsNotPaid/index.html.md)
- [updateClaimAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemValidationStep/index.html.md)
- [updateClaimAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemWorkflow/index.html.md)
- [updateClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemValidationStep/index.html.md)
- [updateClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemWorkflow/index.html.md)
- [updateClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodValidationStep/index.html.md)
- [updateClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodWorkflow/index.html.md)
- [updateExchangeAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemValidationStep/index.html.md)
- [updateExchangeAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemWorkflow/index.html.md)
- [updateExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodValidationStep/index.html.md)
- [updateExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodWorkflow/index.html.md)
- [updateOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangeActionsWorkflow/index.html.md)
- [updateOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangeWorkflow/index.html.md)
- [updateOrderChangesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangesWorkflow/index.html.md)
- [updateOrderEditAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemValidationStep/index.html.md)
- [updateOrderEditAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemWorkflow/index.html.md)
- [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md)
- [updateOrderEditItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityWorkflow/index.html.md)
- [updateOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodValidationStep/index.html.md)
- [updateOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodWorkflow/index.html.md)
- [updateOrderTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderTaxLinesWorkflow/index.html.md)
- [updateOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderValidationStep/index.html.md)
- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md)
- [updateReceiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestValidationStep/index.html.md)
- [updateReceiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestWorkflow/index.html.md)
- [updateRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnValidationStep/index.html.md)
- [updateRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnWorkflow/index.html.md)
- [updateReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodValidationStep/index.html.md)
- [updateReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodWorkflow/index.html.md)
- [updateReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnValidationStep/index.html.md)
- [updateReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnWorkflow/index.html.md)
- [validateCarryPromotionsFlagStep](https://docs.medusajs.com/references/medusa-workflows/validateCarryPromotionsFlagStep/index.html.md)
- [validateOrderCreditLinesStep](https://docs.medusajs.com/references/medusa-workflows/validateOrderCreditLinesStep/index.html.md)
- [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md)
- [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md)
- [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md)
- [refundPaymentsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentsWorkflow/index.html.md)
- [validatePaymentsRefundStep](https://docs.medusajs.com/references/medusa-workflows/validatePaymentsRefundStep/index.html.md)
- [validateRefundPaymentExceedsCapturedAmountStep](https://docs.medusajs.com/references/medusa-workflows/validateRefundPaymentExceedsCapturedAmountStep/index.html.md)
- [createPaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentSessionsWorkflow/index.html.md)
- [createRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRefundReasonsWorkflow/index.html.md)
- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/index.html.md)
- [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/index.html.md)
- [updateRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRefundReasonsWorkflow/index.html.md)
- [batchPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPriceListPricesWorkflow/index.html.md)
- [createPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListPricesWorkflow/index.html.md)
- [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/index.html.md)
- [deletePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePriceListsWorkflow/index.html.md)
- [removePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/removePriceListPricesWorkflow/index.html.md)
- [updatePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListPricesWorkflow/index.html.md)
- [updatePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListsWorkflow/index.html.md)
- [createPricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPricePreferencesWorkflow/index.html.md)
- [deletePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePricePreferencesWorkflow/index.html.md)
- [updatePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePricePreferencesWorkflow/index.html.md)
- [batchImageVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchImageVariantsWorkflow/index.html.md)
- [batchLinkProductsToCategoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCategoryWorkflow/index.html.md)
- [batchLinkProductsToCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCollectionWorkflow/index.html.md)
- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md)
- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md)
- [batchVariantImagesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchVariantImagesWorkflow/index.html.md)
- [createCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCollectionsWorkflow/index.html.md)
- [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/index.html.md)
- [createProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTagsWorkflow/index.html.md)
- [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/index.html.md)
- [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md)
- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md)
- [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md)
- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md)
- [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md)
- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md)
- [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/index.html.md)
- [deleteProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductsWorkflow/index.html.md)
- [exportProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/exportProductsWorkflow/index.html.md)
- [importProductsAsChunksWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsAsChunksWorkflow/index.html.md)
- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md)
- [updateCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCollectionsWorkflow/index.html.md)
- [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/index.html.md)
- [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/index.html.md)
- [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md)
- [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/index.html.md)
- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md)
- [upsertVariantPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/upsertVariantPricesWorkflow/index.html.md)
- [validateProductInputStep](https://docs.medusajs.com/references/medusa-workflows/validateProductInputStep/index.html.md)
- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md)
- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md)
- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/index.html.md)
- [addOrRemoveCampaignPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrRemoveCampaignPromotionsWorkflow/index.html.md)
- [batchPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPromotionRulesWorkflow/index.html.md)
- [createCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCampaignsWorkflow/index.html.md)
- [createPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionRulesWorkflow/index.html.md)
- [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md)
- [deleteCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCampaignsWorkflow/index.html.md)
- [deletePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionRulesWorkflow/index.html.md)
- [deletePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionsWorkflow/index.html.md)
- [updateCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCampaignsWorkflow/index.html.md)
- [updatePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionRulesWorkflow/index.html.md)
- [updatePromotionsStatusWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsStatusWorkflow/index.html.md)
- [updatePromotionsValidationStep](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsValidationStep/index.html.md)
- [updatePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsWorkflow/index.html.md)
- [createRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRegionsWorkflow/index.html.md)
- [deleteRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRegionsWorkflow/index.html.md)
- [updateRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRegionsWorkflow/index.html.md)
- [createReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReservationsWorkflow/index.html.md)
- [deleteReservationsByLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsByLineItemsWorkflow/index.html.md)
- [deleteReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsWorkflow/index.html.md)
- [updateReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReservationsWorkflow/index.html.md)
- [createReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnReasonsWorkflow/index.html.md)
- [deleteReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReturnReasonsWorkflow/index.html.md)
- [updateReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnReasonsWorkflow/index.html.md)
- [createSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createSalesChannelsWorkflow/index.html.md)
- [deleteSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteSalesChannelsWorkflow/index.html.md)
- [linkProductsToSalesChannelWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkProductsToSalesChannelWorkflow/index.html.md)
- [updateSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateSalesChannelsWorkflow/index.html.md)
- [createViewConfigurationWorkflow](https://docs.medusajs.com/references/medusa-workflows/createViewConfigurationWorkflow/index.html.md)
- [updateViewConfigurationWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateViewConfigurationWorkflow/index.html.md)
- [createShippingOptionTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingOptionTypesWorkflow/index.html.md)
- [deleteShippingOptionTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingOptionTypesWorkflow/index.html.md)
- [updateShippingOptionTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingOptionTypesWorkflow/index.html.md)
- [deleteShippingProfileWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingProfileWorkflow/index.html.md)
- [validateStepShippingProfileDelete](https://docs.medusajs.com/references/medusa-workflows/validateStepShippingProfileDelete/index.html.md)
- [createLocationFulfillmentSetWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLocationFulfillmentSetWorkflow/index.html.md)
- [createStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md)
- [deleteStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStockLocationsWorkflow/index.html.md)
- [linkSalesChannelsToStockLocationWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToStockLocationWorkflow/index.html.md)
- [updateStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStockLocationsWorkflow/index.html.md)
- [createStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStoresWorkflow/index.html.md)
- [deleteStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStoresWorkflow/index.html.md)
- [updateStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStoresWorkflow/index.html.md)
- [createTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRateRulesWorkflow/index.html.md)
- [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/index.html.md)
- [createTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRegionsWorkflow/index.html.md)
- [deleteTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRateRulesWorkflow/index.html.md)
- [deleteTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRatesWorkflow/index.html.md)
- [deleteTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRegionsWorkflow/index.html.md)
- [maybeListTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/maybeListTaxRateRuleIdsStep/index.html.md)
- [setTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/setTaxRateRulesWorkflow/index.html.md)
- [updateTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRatesWorkflow/index.html.md)
- [updateTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRegionsWorkflow/index.html.md)
- [batchTranslationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchTranslationsWorkflow/index.html.md)
- [createTranslationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTranslationsWorkflow/index.html.md)
- [deleteTranslationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTranslationsWorkflow/index.html.md)
- [updateTranslationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTranslationsWorkflow/index.html.md)
- [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md)
- [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md)
- [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md)
- [removeUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeUserAccountWorkflow/index.html.md)
- [updateUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateUsersWorkflow/index.html.md)


## Steps

- [createApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/createApiKeysStep/index.html.md)
- [deleteApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteApiKeysStep/index.html.md)
- [linkSalesChannelsToApiKeyStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkSalesChannelsToApiKeyStep/index.html.md)
- [revokeApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/revokeApiKeysStep/index.html.md)
- [updateApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateApiKeysStep/index.html.md)
- [validateSalesChannelsExistStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateSalesChannelsExistStep/index.html.md)
- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md)
- [addShippingMethodToCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/addShippingMethodToCartStep/index.html.md)
- [confirmInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/confirmInventoryStep/index.html.md)
- [createCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCartsStep/index.html.md)
- [createLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemAdjustmentsStep/index.html.md)
- [createLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemsStep/index.html.md)
- [createPaymentCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentCollectionsStep/index.html.md)
- [createShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingMethodAdjustmentsStep/index.html.md)
- [findOneOrAnyRegionStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOneOrAnyRegionStep/index.html.md)
- [findOrCreateCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOrCreateCustomerStep/index.html.md)
- [findSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/findSalesChannelStep/index.html.md)
- [getActionsToComputeFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getActionsToComputeFromPromotionsStep/index.html.md)
- [getLineItemActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getLineItemActionsStep/index.html.md)
- [getPromotionCodesToApply](https://docs.medusajs.com/references/medusa-workflows/steps/getPromotionCodesToApply/index.html.md)
- [getVariantPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantPriceSetsStep/index.html.md)
- [getVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantsStep/index.html.md)
- [prepareAdjustmentsFromPromotionActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/prepareAdjustmentsFromPromotionActionsStep/index.html.md)
- [removeLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeLineItemAdjustmentsStep/index.html.md)
- [removeShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodAdjustmentsStep/index.html.md)
- [removeShippingMethodFromCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodFromCartStep/index.html.md)
- [reserveInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/reserveInventoryStep/index.html.md)
- [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/index.html.md)
- [setTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setTaxLinesForItemsStep/index.html.md)
- [updateCartItemsTranslationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartItemsTranslationsStep/index.html.md)
- [updateCartPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartPromotionsStep/index.html.md)
- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md)
- [updateLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStep/index.html.md)
- [updateShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingMethodsStep/index.html.md)
- [validateAndReturnShippingMethodsDataStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateAndReturnShippingMethodsDataStep/index.html.md)
- [validateCartPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartPaymentsStep/index.html.md)
- [validateCartShippingOptionsPriceStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsPriceStep/index.html.md)
- [validateCartShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsStep/index.html.md)
- [validateCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartStep/index.html.md)
- [validateLineItemPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateLineItemPricesStep/index.html.md)
- [validateShippingStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingStep/index.html.md)
- [validateVariantPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPricesStep/index.html.md)
- [createEntitiesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createEntitiesStep/index.html.md)
- [createRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRemoteLinkStep/index.html.md)
- [deleteEntitiesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteEntitiesStep/index.html.md)
- [dismissRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/dismissRemoteLinkStep/index.html.md)
- [emitEventStep](https://docs.medusajs.com/references/medusa-workflows/steps/emitEventStep/index.html.md)
- [getTranslatedLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getTranslatedLineItemsStep/index.html.md)
- [getTranslatedShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getTranslatedShippingOptionsStep/index.html.md)
- [removeRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRemoteLinkStep/index.html.md)
- [updateRemoteLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRemoteLinksStep/index.html.md)
- [useQueryGraphStep](https://docs.medusajs.com/references/medusa-workflows/steps/useQueryGraphStep/index.html.md)
- [useRemoteQueryStep](https://docs.medusajs.com/references/medusa-workflows/steps/useRemoteQueryStep/index.html.md)
- [validatePresenceOfStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePresenceOfStep/index.html.md)
- [createCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerAddressesStep/index.html.md)
- [createCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomersStep/index.html.md)
- [deleteCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerAddressesStep/index.html.md)
- [deleteCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomersStep/index.html.md)
- [maybeUnsetDefaultBillingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultBillingAddressesStep/index.html.md)
- [maybeUnsetDefaultShippingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultShippingAddressesStep/index.html.md)
- [updateCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerAddressesStep/index.html.md)
- [updateCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomersStep/index.html.md)
- [validateCustomerAccountCreation](https://docs.medusajs.com/references/medusa-workflows/steps/validateCustomerAccountCreation/index.html.md)
- [createCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerGroupsStep/index.html.md)
- [deleteCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerGroupStep/index.html.md)
- [linkCustomerGroupsToCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomerGroupsToCustomerStep/index.html.md)
- [linkCustomersToCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomersToCustomerGroupStep/index.html.md)
- [updateCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerGroupsStep/index.html.md)
- [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/index.html.md)
- [deleteDraftOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteDraftOrdersStep/index.html.md)
- [validateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDraftOrderStep/index.html.md)
- [deleteFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFilesStep/index.html.md)
- [uploadFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/uploadFilesStep/index.html.md)
- [buildPriceSet](https://docs.medusajs.com/references/medusa-workflows/steps/buildPriceSet/index.html.md)
- [calculateShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/calculateShippingOptionsPricesStep/index.html.md)
- [cancelFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelFulfillmentStep/index.html.md)
- [createFulfillmentSets](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentSets/index.html.md)
- [createFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentStep/index.html.md)
- [createReturnFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnFulfillmentStep/index.html.md)
- [createServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createServiceZonesStep/index.html.md)
- [createShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionRulesStep/index.html.md)
- [createShippingOptionsPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionsPriceSetsStep/index.html.md)
- [createShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingProfilesStep/index.html.md)
- [deleteFulfillmentSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFulfillmentSetsStep/index.html.md)
- [deleteServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteServiceZonesStep/index.html.md)
- [deleteShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionRulesStep/index.html.md)
- [deleteShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionsStep/index.html.md)
- [setShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/setShippingOptionsPricesStep/index.html.md)
- [updateFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateFulfillmentStep/index.html.md)
- [updateServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateServiceZonesStep/index.html.md)
- [updateShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingOptionRulesStep/index.html.md)
- [updateShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingProfilesStep/index.html.md)
- [upsertShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/upsertShippingOptionsStep/index.html.md)
- [validateShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShipmentStep/index.html.md)
- [validateShippingOptionPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingOptionPricesStep/index.html.md)
- [adjustInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/adjustInventoryLevelsStep/index.html.md)
- [attachInventoryItemToVariants](https://docs.medusajs.com/references/medusa-workflows/steps/attachInventoryItemToVariants/index.html.md)
- [createInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryItemsStep/index.html.md)
- [createInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryLevelsStep/index.html.md)
- [deleteInventoryItemStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryItemStep/index.html.md)
- [deleteInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryLevelsStep/index.html.md)
- [updateInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryItemsStep/index.html.md)
- [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/index.html.md)
- [validateInventoryDeleteStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryDeleteStep/index.html.md)
- [validateInventoryItemsForCreate](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryItemsForCreate/index.html.md)
- [validateInventoryLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryLocationsStep/index.html.md)
- [createInviteStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInviteStep/index.html.md)
- [deleteInvitesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInvitesStep/index.html.md)
- [refreshInviteTokensStep](https://docs.medusajs.com/references/medusa-workflows/steps/refreshInviteTokensStep/index.html.md)
- [validateTokenStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateTokenStep/index.html.md)
- [deleteLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteLineItemsStep/index.html.md)
- [listLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listLineItemsStep/index.html.md)
- [updateLineItemsStepWithSelector](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStepWithSelector/index.html.md)
- [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md)
- [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md)
- [notifyOnFailureStep](https://docs.medusajs.com/references/medusa-workflows/steps/notifyOnFailureStep/index.html.md)
- [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md)
- [addOrderTransactionStep](https://docs.medusajs.com/references/medusa-workflows/steps/addOrderTransactionStep/index.html.md)
- [archiveOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/archiveOrdersStep/index.html.md)
- [cancelOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderChangeStep/index.html.md)
- [cancelOrderClaimStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderClaimStep/index.html.md)
- [cancelOrderExchangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderExchangeStep/index.html.md)
- [cancelOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderFulfillmentStep/index.html.md)
- [cancelOrderReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderReturnStep/index.html.md)
- [cancelOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrdersStep/index.html.md)
- [completeOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/completeOrdersStep/index.html.md)
- [createCompleteReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCompleteReturnStep/index.html.md)
- [createOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderChangeStep/index.html.md)
- [createOrderClaimItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimItemsFromActionsStep/index.html.md)
- [createOrderClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimsStep/index.html.md)
- [createOrderExchangeItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangeItemsFromActionsStep/index.html.md)
- [createOrderExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangesStep/index.html.md)
- [createOrderLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderLineItemsStep/index.html.md)
- [createOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrdersStep/index.html.md)
- [createReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnsStep/index.html.md)
- [declineOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/declineOrderChangeStep/index.html.md)
- [deleteClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteClaimsStep/index.html.md)
- [deleteExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteExchangesStep/index.html.md)
- [deleteOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangeActionsStep/index.html.md)
- [deleteOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangesStep/index.html.md)
- [deleteOrderLineItems](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderLineItems/index.html.md)
- [deleteOrderShippingMethods](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderShippingMethods/index.html.md)
- [deleteReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnsStep/index.html.md)
- [exportOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/exportOrdersStep/index.html.md)
- [listOrderChangeActionsByTypeStep](https://docs.medusajs.com/references/medusa-workflows/steps/listOrderChangeActionsByTypeStep/index.html.md)
- [previewOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/previewOrderChangeStep/index.html.md)
- [registerOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderChangesStep/index.html.md)
- [registerOrderDeliveryStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderDeliveryStep/index.html.md)
- [registerOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderFulfillmentStep/index.html.md)
- [registerOrderShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderShipmentStep/index.html.md)
- [setOrderTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setOrderTaxLinesForItemsStep/index.html.md)
- [updateOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangeActionsStep/index.html.md)
- [updateOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangesStep/index.html.md)
- [updateOrderItemsTranslationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderItemsTranslationsStep/index.html.md)
- [updateOrderShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderShippingMethodsStep/index.html.md)
- [updateOrderShippingMethodsTranslationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderShippingMethodsTranslationsStep/index.html.md)
- [updateOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrdersStep/index.html.md)
- [updateReturnItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnItemsStep/index.html.md)
- [updateReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnsStep/index.html.md)
- [authorizePaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/authorizePaymentSessionStep/index.html.md)
- [cancelPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelPaymentStep/index.html.md)
- [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/index.html.md)
- [refundPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentStep/index.html.md)
- [refundPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentsStep/index.html.md)
- [createPaymentAccountHolderStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentAccountHolderStep/index.html.md)
- [createPaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentSessionStep/index.html.md)
- [createRefundReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRefundReasonStep/index.html.md)
- [deletePaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePaymentSessionsStep/index.html.md)
- [deleteRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRefundReasonsStep/index.html.md)
- [updatePaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePaymentCollectionStep/index.html.md)
- [updateRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRefundReasonsStep/index.html.md)
- [validateDeletedPaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDeletedPaymentSessionsStep/index.html.md)
- [createPriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListPricesStep/index.html.md)
- [createPriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListsStep/index.html.md)
- [deletePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePriceListsStep/index.html.md)
- [getExistingPriceListsPriceIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getExistingPriceListsPriceIdsStep/index.html.md)
- [removePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/removePriceListPricesStep/index.html.md)
- [updatePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListPricesStep/index.html.md)
- [updatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListsStep/index.html.md)
- [validatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePriceListsStep/index.html.md)
- [validateVariantPriceLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPriceLinksStep/index.html.md)
- [createPricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPricePreferencesStep/index.html.md)
- [createPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceSetsStep/index.html.md)
- [deletePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePricePreferencesStep/index.html.md)
- [updatePricePreferencesAsArrayStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesAsArrayStep/index.html.md)
- [updatePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesStep/index.html.md)
- [updatePriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceSetsStep/index.html.md)
- [addImageToVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addImageToVariantsStep/index.html.md)
- [addImagesToVariantStep](https://docs.medusajs.com/references/medusa-workflows/steps/addImagesToVariantStep/index.html.md)
- [batchLinkProductsToCategoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCategoryStep/index.html.md)
- [batchLinkProductsToCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCollectionStep/index.html.md)
- [createCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCollectionsStep/index.html.md)
- [createProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductOptionsStep/index.html.md)
- [createProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTagsStep/index.html.md)
- [createProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTypesStep/index.html.md)
- [createProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductVariantsStep/index.html.md)
- [createProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductsStep/index.html.md)
- [createVariantPricingLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createVariantPricingLinkStep/index.html.md)
- [deleteCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCollectionsStep/index.html.md)
- [deleteProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductOptionsStep/index.html.md)
- [deleteProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTagsStep/index.html.md)
- [deleteProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTypesStep/index.html.md)
- [deleteProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductVariantsStep/index.html.md)
- [deleteProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductsStep/index.html.md)
- [dismissProductVariantsInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/dismissProductVariantsInventoryStep/index.html.md)
- [generateProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/generateProductCsvStep/index.html.md)
- [getAllProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getAllProductsStep/index.html.md)
- [getProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getProductsStep/index.html.md)
- [getVariantAvailabilityStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantAvailabilityStep/index.html.md)
- [normalizeCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/normalizeCsvStep/index.html.md)
- [normalizeCsvToChunksStep](https://docs.medusajs.com/references/medusa-workflows/steps/normalizeCsvToChunksStep/index.html.md)
- [parseProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/parseProductCsvStep/index.html.md)
- [processImportChunksStep](https://docs.medusajs.com/references/medusa-workflows/steps/processImportChunksStep/index.html.md)
- [removeImageFromVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeImageFromVariantsStep/index.html.md)
- [removeImagesFromVariantStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeImagesFromVariantStep/index.html.md)
- [updateCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCollectionsStep/index.html.md)
- [updateProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductOptionsStep/index.html.md)
- [updateProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTagsStep/index.html.md)
- [updateProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTypesStep/index.html.md)
- [updateProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductVariantsStep/index.html.md)
- [updateProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductsStep/index.html.md)
- [waitConfirmationProductImportStep](https://docs.medusajs.com/references/medusa-workflows/steps/waitConfirmationProductImportStep/index.html.md)
- [createProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductCategoriesStep/index.html.md)
- [deleteProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductCategoriesStep/index.html.md)
- [updateProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductCategoriesStep/index.html.md)
- [addCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addCampaignPromotionsStep/index.html.md)
- [addRulesToPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addRulesToPromotionsStep/index.html.md)
- [createCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCampaignsStep/index.html.md)
- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md)
- [deleteCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCampaignsStep/index.html.md)
- [deletePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePromotionsStep/index.html.md)
- [removeCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeCampaignPromotionsStep/index.html.md)
- [removeRulesFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRulesFromPromotionsStep/index.html.md)
- [updateCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCampaignsStep/index.html.md)
- [updatePromotionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionRulesStep/index.html.md)
- [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md)
- [createRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRegionsStep/index.html.md)
- [deleteRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRegionsStep/index.html.md)
- [setRegionsPaymentProvidersStep](https://docs.medusajs.com/references/medusa-workflows/steps/setRegionsPaymentProvidersStep/index.html.md)
- [updateRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRegionsStep/index.html.md)
- [createReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReservationsStep/index.html.md)
- [deleteReservationsByLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsByLineItemsStep/index.html.md)
- [deleteReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsStep/index.html.md)
- [updateReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReservationsStep/index.html.md)
- [createReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnReasonsStep/index.html.md)
- [deleteReturnReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnReasonStep/index.html.md)
- [updateReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnReasonsStep/index.html.md)
- [associateLocationsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateLocationsWithSalesChannelsStep/index.html.md)
- [associateProductsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateProductsWithSalesChannelsStep/index.html.md)
- [canDeleteSalesChannelsOrThrowStep](https://docs.medusajs.com/references/medusa-workflows/steps/canDeleteSalesChannelsOrThrowStep/index.html.md)
- [createDefaultSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultSalesChannelStep/index.html.md)
- [createSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createSalesChannelsStep/index.html.md)
- [deleteSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteSalesChannelsStep/index.html.md)
- [detachLocationsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachLocationsFromSalesChannelsStep/index.html.md)
- [detachProductsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachProductsFromSalesChannelsStep/index.html.md)
- [updateSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateSalesChannelsStep/index.html.md)
- [createViewConfigurationStep](https://docs.medusajs.com/references/medusa-workflows/steps/createViewConfigurationStep/index.html.md)
- [setActiveViewConfigurationStep](https://docs.medusajs.com/references/medusa-workflows/steps/setActiveViewConfigurationStep/index.html.md)
- [updateViewConfigurationStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateViewConfigurationStep/index.html.md)
- [createShippingOptionTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionTypesStep/index.html.md)
- [deleteShippingOptionTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionTypesStep/index.html.md)
- [listShippingOptionsForContextStep](https://docs.medusajs.com/references/medusa-workflows/steps/listShippingOptionsForContextStep/index.html.md)
- [updateShippingOptionTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingOptionTypesStep/index.html.md)
- [deleteShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingProfilesStep/index.html.md)
- [createStockLocations](https://docs.medusajs.com/references/medusa-workflows/steps/createStockLocations/index.html.md)
- [deleteStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStockLocationsStep/index.html.md)
- [updateStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStockLocationsStep/index.html.md)
- [createStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/createStoresStep/index.html.md)
- [deleteStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStoresStep/index.html.md)
- [updateStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStoresStep/index.html.md)
- [createTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRateRulesStep/index.html.md)
- [createTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRatesStep/index.html.md)
- [createTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRegionsStep/index.html.md)
- [deleteTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRateRulesStep/index.html.md)
- [deleteTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRatesStep/index.html.md)
- [deleteTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRegionsStep/index.html.md)
- [getItemTaxLinesStep](https://docs.medusajs.com/references/medusa-workflows/steps/getItemTaxLinesStep/index.html.md)
- [listTaxRateIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateIdsStep/index.html.md)
- [listTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateRuleIdsStep/index.html.md)
- [updateTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRatesStep/index.html.md)
- [updateTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRegionsStep/index.html.md)
- [createTranslationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTranslationsStep/index.html.md)
- [deleteTranslationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTranslationsStep/index.html.md)
- [updateTranslationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTranslationsStep/index.html.md)
- [validateTranslationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateTranslationsStep/index.html.md)
- [createUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createUsersStep/index.html.md)
- [deleteUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteUsersStep/index.html.md)
- [updateUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateUsersStep/index.html.md)


# Events Reference

This documentation page includes the list of all events emitted by [Medusa's workflows](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md).

## Auth Events

### Summary

|Event|Description|
|---|---|
|auth.password\_reset|Emitted when a reset password token is generated. You can listen to this event
to send a reset password email to the user or customer, for example.|

### auth.password\_reset

Emitted when a reset password token is generated. You can listen to this event
to send a reset password email to the user or customer, for example.

#### Payload

```ts
{
  entity_id, // The identifier of the user or customer. For example, an email address.
  actor_type, // The type of actor. For example, "customer", "user", or custom.
  token, // The generated token.
  metadata, // Optional custom metadata passed from the request.
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md)

***

## Cart Events

### Summary

|Event|Description|
|---|---|
|cart.created|Emitted when a cart is created.|
|cart.updated|Emitted when a cart's details are updated.|
|cart.region\_updated|Emitted when the cart's region is updated. This
event is emitted alongside the |
|cart.customer\_transferred|Emitted when the customer in the cart is transferred.|

### cart.created

Emitted when a cart is created.

#### Payload

```ts
{
  id, // The ID of the cart
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md)

***

### cart.updated

Emitted when a cart's details are updated.

#### Payload

```ts
{
  id, // The ID of the cart
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/index.html.md)
- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md)
- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md)
- [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md)

***

### cart.region\_updated

Emitted when the cart's region is updated. This
event is emitted alongside the `cart.updated` event.

#### Payload

```ts
{
  id, // The ID of the cart
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md)

***

### cart.customer\_transferred&#xA;

Emitted when the customer in the cart is transferred.

#### Payload

```ts
{
  id, // The ID of the cart
  customer_id, // The ID of the customer
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md)

***

## Customer Events

### Summary

|Event|Description|
|---|---|
|customer.created|Emitted when a customer is created.|
|customer.updated|Emitted when a customer is updated.|
|customer.deleted|Emitted when a customer is deleted.|

### customer.created

Emitted when a customer is created.

#### Payload

```ts
{
  id, // The ID of the customer
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/index.html.md)
- [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md)

***

### customer.updated

Emitted when a customer is updated.

#### Payload

```ts
{
  id, // The ID of the customer
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md)

***

### customer.deleted

Emitted when a customer is deleted.

#### Payload

```ts
{
  id, // The ID of the customer
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomersWorkflow/index.html.md)
- [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/index.html.md)

***

## Fulfillment Events

### Summary

|Event|Description|
|---|---|
|shipment.created|Emitted when a shipment is created for an order.|
|delivery.created|Emitted when a fulfillment is marked as delivered.|

### shipment.created

Emitted when a shipment is created for an order.

#### Payload

```ts
{
  id, // the ID of the fulfillment
  no_notification, // (boolean) whether to notify the customer
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/index.html.md)

***

### delivery.created

Emitted when a fulfillment is marked as delivered.

#### Payload

```ts
{
  id, // the ID of the fulfillment
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md)

***

## Invite Events

### Summary

|Event|Description|
|---|---|
|invite.accepted|Emitted when an invite is accepted.|
|invite.created|Emitted when invites are created. You can listen to this event
to send an email to the invited users, for example.|
|invite.deleted|Emitted when invites are deleted.|
|invite.resent|Emitted when invites should be resent because their token was
refreshed. You can listen to this event to send an email to the invited users,
for example.|

### invite.accepted

Emitted when an invite is accepted.

#### Payload

```ts
{
  id, // The ID of the invite
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md)

***

### invite.created

Emitted when invites are created. You can listen to this event
to send an email to the invited users, for example.

#### Payload

```ts
{
  id, // The ID of the invite
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInvitesWorkflow/index.html.md)

***

### invite.deleted

Emitted when invites are deleted.

#### Payload

```ts
{
  id, // The ID of the invite
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/index.html.md)

***

### invite.resent

Emitted when invites should be resent because their token was
refreshed. You can listen to this event to send an email to the invited users,
for example.

#### Payload

```ts
{
  id, // The ID of the invite
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/index.html.md)

***

## Order Edit Events

### Summary

|Event|Description|
|---|---|
|order-edit.requested|Emitted when an order edit is requested.|
|order-edit.confirmed|Emitted when an order edit request is confirmed.|
|order-edit.canceled|Emitted when an order edit request is canceled.|

### order-edit.requested&#xA;

Emitted when an order edit is requested.

#### Payload

```ts
{
  order_id, // The ID of the order
  actions, // (array) The [actions](https://docs.medusajs.com/resources/references/fulfillment/interfaces/fulfillment.OrderChangeActionDTO) to edit the order
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [requestOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestWorkflow/index.html.md)

***

### order-edit.confirmed&#xA;

Emitted when an order edit request is confirmed.

#### Payload

```ts
{
  order_id, // The ID of the order
  actions, // (array) The [actions](https://docs.medusajs.com/resources/references/fulfillment/interfaces/fulfillment.OrderChangeActionDTO) to edit the order
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md)

***

### order-edit.canceled&#xA;

Emitted when an order edit request is canceled.

#### Payload

```ts
{
  order_id, // The ID of the order
  actions, // (array) The [actions](https://docs.medusajs.com/resources/references/fulfillment/interfaces/fulfillment.OrderChangeActionDTO) to edit the order
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/index.html.md)

***

## Order Events

### Summary

|Event|Description|
|---|---|
|order.updated|Emitted when the details of an order or draft order is updated. This
doesn't include updates made by an edit.|
|order.placed|Emitted when an order is placed, or when a draft order is converted to an
order.|
|order.canceled|Emitted when an order is canceld.|
|order.completed|Emitted when orders are completed.|
|order.archived|Emitted when an order is archived.|
|order.fulfillment\_created|Emitted when a fulfillment is created for an order.|
|order.fulfillment\_canceled|Emitted when an order's fulfillment is canceled.|
|order.return\_requested|Emitted when a return request is confirmed.|
|order.return\_received|Emitted when a return is marked as received.|
|order.claim\_created|Emitted when a claim is created for an order.|
|order.exchange\_created|Emitted when an exchange is created for an order.|
|order.transfer\_requested|Emitted when an order is requested to be transferred to
another customer.|

### order.updated

Emitted when the details of an order or draft order is updated. This
doesn't include updates made by an edit.

#### Payload

```ts
{
  id, // The ID of the order
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md)
- [updateDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderWorkflow/index.html.md)

***

### order.placed

Emitted when an order is placed, or when a draft order is converted to an
order.

#### Payload

```ts
{
  id, // The ID of the order
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [convertDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderWorkflow/index.html.md)
- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md)

***

### order.canceled

Emitted when an order is canceld.

#### Payload

```ts
{
  id, // The ID of the order
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/index.html.md)

***

### order.completed

Emitted when orders are completed.

#### Payload

```ts
{
  id, // The ID of the order
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md)

***

### order.archived

Emitted when an order is archived.

#### Payload

```ts
{
  id, // The ID of the order
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md)

***

### order.fulfillment\_created

Emitted when a fulfillment is created for an order.

#### Payload

```ts
{
  order_id, // The ID of the order
  fulfillment_id, // The ID of the fulfillment
  no_notification, // (boolean) Whether to notify the customer
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md)

***

### order.fulfillment\_canceled

Emitted when an order's fulfillment is canceled.

#### Payload

```ts
{
  order_id, // The ID of the order
  fulfillment_id, // The ID of the fulfillment
  no_notification, // (boolean) Whether to notify the customer
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md)

***

### order.return\_requested

Emitted when a return request is confirmed.

#### Payload

```ts
{
  order_id, // The ID of the order
  return_id, // The ID of the return
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md)
- [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md)

***

### order.return\_received

Emitted when a return is marked as received.

#### Payload

```ts
{
  order_id, // The ID of the order
  return_id, // The ID of the return
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md)
- [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md)

***

### order.claim\_created

Emitted when a claim is created for an order.

#### Payload

```ts
{
  order_id, // The ID of the order
  claim_id, // The ID of the claim
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [confirmClaimRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestWorkflow/index.html.md)

***

### order.exchange\_created

Emitted when an exchange is created for an order.

#### Payload

```ts
{
  order_id, // The ID of the order
  exchange_id, // The ID of the exchange
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md)

***

### order.transfer\_requested

Emitted when an order is requested to be transferred to
another customer.

#### Payload

```ts
{
  id, // The ID of the order
  order_change_id, // The ID of the order change created for the transfer
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [requestOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferWorkflow/index.html.md)

***

## Payment Events

### Summary

|Event|Description|
|---|---|
|payment.captured|Emitted when a payment is captured.|
|payment.refunded|Emitted when a payment is refunded.|

### payment.captured

Emitted when a payment is captured.

#### Payload

```ts
{
  id, // the ID of the payment
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md)
- [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md)
- [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/index.html.md)

***

### payment.refunded

Emitted when a payment is refunded.

#### Payload

```ts
{
  id, // the ID of the payment
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md)

***

## Product Category Events

### Summary

|Event|Description|
|---|---|
|product-category.created|Emitted when product categories are created.|
|product-category.updated|Emitted when product categories are updated.|
|product-category.deleted|Emitted when product categories are deleted.|

### product-category.created

Emitted when product categories are created.

#### Payload

```ts
{
  id, // The ID of the product category
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md)

***

### product-category.updated

Emitted when product categories are updated.

#### Payload

```ts
{
  id, // The ID of the product category
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/index.html.md)

***

### product-category.deleted

Emitted when product categories are deleted.

#### Payload

```ts
{
  id, // The ID of the product category
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md)

***

## Product Collection Events

### Summary

|Event|Description|
|---|---|
|product-collection.created|Emitted when product collections are created.|
|product-collection.updated|Emitted when product collections are updated.|
|product-collection.deleted|Emitted when product collections are deleted.|

### product-collection.created

Emitted when product collections are created.

#### Payload

```ts
{
  id, // The ID of the product collection
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCollectionsWorkflow/index.html.md)

***

### product-collection.updated

Emitted when product collections are updated.

#### Payload

```ts
{
  id, // The ID of the product collection
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCollectionsWorkflow/index.html.md)

***

### product-collection.deleted

Emitted when product collections are deleted.

#### Payload

```ts
{
  id, // The ID of the product collection
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md)

***

## Product Option Events

### Summary

|Event|Description|
|---|---|
|product-option.updated|Emitted when product options are updated.|
|product-option.created|Emitted when product options are created.|
|product-option.deleted|Emitted when product options are deleted.|

### product-option.updated

Emitted when product options are updated.

#### Payload

```ts
{
  id, // The ID of the product option
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/index.html.md)

***

### product-option.created

Emitted when product options are created.

#### Payload

```ts
{
  id, // The ID of the product option
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/index.html.md)

***

### product-option.deleted

Emitted when product options are deleted.

#### Payload

```ts
{
  id, // The ID of the product option
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md)

***

## Product Tag Events

### Summary

|Event|Description|
|---|---|
|product-tag.updated|Emitted when product tags are updated.|
|product-tag.created|Emitted when product tags are created.|
|product-tag.deleted|Emitted when product tags are deleted.|

### product-tag.updated

Emitted when product tags are updated.

#### Payload

```ts
{
  id, // The ID of the product tag
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/index.html.md)

***

### product-tag.created

Emitted when product tags are created.

#### Payload

```ts
{
  id, // The ID of the product tag
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTagsWorkflow/index.html.md)

***

### product-tag.deleted

Emitted when product tags are deleted.

#### Payload

```ts
{
  id, // The ID of the product tag
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md)

***

## Product Type Events

### Summary

|Event|Description|
|---|---|
|product-type.updated|Emitted when product types are updated.|
|product-type.created|Emitted when product types are created.|
|product-type.deleted|Emitted when product types are deleted.|

### product-type.updated

Emitted when product types are updated.

#### Payload

```ts
{
  id, // The ID of the product type
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md)

***

### product-type.created

Emitted when product types are created.

#### Payload

```ts
{
  id, // The ID of the product type
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/index.html.md)

***

### product-type.deleted

Emitted when product types are deleted.

#### Payload

```ts
{
  id, // The ID of the product type
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md)

***

## Product Variant Events

### Summary

|Event|Description|
|---|---|
|product-variant.updated|Emitted when product variants are updated.|
|product-variant.created|Emitted when product variants are created.|
|product-variant.deleted|Emitted when product variants are deleted.|

### product-variant.updated

Emitted when product variants are updated.

#### Payload

```ts
{
  id, // The ID of the product variant
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/index.html.md)
- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md)

***

### product-variant.created

Emitted when product variants are created.

#### Payload

```ts
{
  id, // The ID of the product variant
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md)
- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md)
- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md)
- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md)
- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md)

***

### product-variant.deleted

Emitted when product variants are deleted.

#### Payload

```ts
{
  id, // The ID of the product variant
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/index.html.md)
- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md)

***

## Product Events

### Summary

|Event|Description|
|---|---|
|product.updated|Emitted when products are updated.|
|product.created|Emitted when products are created.|
|product.deleted|Emitted when products are deleted.|

### product.updated

Emitted when products are updated.

#### Payload

```ts
{
  id, // The ID of the product
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md)
- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md)
- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md)

***

### product.created

Emitted when products are created.

#### Payload

```ts
{
  id, // The ID of the product
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md)
- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md)
- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md)

***

### product.deleted

Emitted when products are deleted.

#### Payload

```ts
{
  id, // The ID of the product
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductsWorkflow/index.html.md)
- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md)
- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md)

***

## Region Events

### Summary

|Event|Description|
|---|---|
|region.updated|Emitted when regions are updated.|
|region.created|Emitted when regions are created.|
|region.deleted|Emitted when regions are deleted.|

### region.updated

Emitted when regions are updated.

#### Payload

```ts
{
  id, // The ID of the region
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRegionsWorkflow/index.html.md)

***

### region.created

Emitted when regions are created.

#### Payload

```ts
{
  id, // The ID of the region
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRegionsWorkflow/index.html.md)

***

### region.deleted

Emitted when regions are deleted.

#### Payload

```ts
{
  id, // The ID of the region
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRegionsWorkflow/index.html.md)

***

## Sales Channel Events

### Summary

|Event|Description|
|---|---|
|sales-channel.created|Emitted when sales channels are created.|
|sales-channel.updated|Emitted when sales channels are updated.|
|sales-channel.deleted|Emitted when sales channels are deleted.|

### sales-channel.created

Emitted when sales channels are created.

#### Payload

```ts
{
  id, // The ID of the sales channel
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createSalesChannelsWorkflow/index.html.md)

***

### sales-channel.updated

Emitted when sales channels are updated.

#### Payload

```ts
{
  id, // The ID of the sales channel
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateSalesChannelsWorkflow/index.html.md)

***

### sales-channel.deleted

Emitted when sales channels are deleted.

#### Payload

```ts
{
  id, // The ID of the sales channel
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteSalesChannelsWorkflow/index.html.md)

***

## Shipping Option Type Events

### Summary

|Event|Description|
|---|---|
|shipping-option-type.updated|Emitted when shipping option types are updated.|
|shipping-option-type.created|Emitted when shipping option types are created.|
|shipping-option-type.deleted|Emitted when shipping option types are deleted.|

### shipping-option-type.updated&#xA;

Emitted when shipping option types are updated.

#### Payload

```ts
{
  id, // The ID of the shipping option type
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateShippingOptionTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingOptionTypesWorkflow/index.html.md)

***

### shipping-option-type.created&#xA;

Emitted when shipping option types are created.

#### Payload

```ts
{
  id, // The ID of the shipping option type
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createShippingOptionTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingOptionTypesWorkflow/index.html.md)

***

### shipping-option-type.deleted&#xA;

Emitted when shipping option types are deleted.

#### Payload

```ts
{
  id, // The ID of the shipping option type
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteShippingOptionTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingOptionTypesWorkflow/index.html.md)

***

## Shipping Option Events

### Summary

|Event|Description|
|---|---|
|shipping-option.created|Emitted when shipping options are created.|
|shipping-option.updated|Emitted when shipping options are updated.|
|shipping-option.deleted|Emitted when shipping options are deleted.|

### shipping-option.created&#xA;

Emitted when shipping options are created.

#### Payload

```ts
{
  id, // The ID of the shipping option
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingOptionsWorkflow/index.html.md)

***

### shipping-option.updated&#xA;

Emitted when shipping options are updated.

#### Payload

```ts
{
  id, // The ID of the shipping option
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingOptionsWorkflow/index.html.md)

***

### shipping-option.deleted&#xA;

Emitted when shipping options are deleted.

#### Payload

```ts
{
  id, // The ID of the shipping option
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingOptionsWorkflow/index.html.md)

***

## Translation Events

### Summary

|Event|Description|
|---|---|
|translation.created|Emitted when translations are created.|
|translation.updated|Emitted when translations are updated.|
|translation.deleted|Emitted when translations are deleted.|

### translation.created&#xA;

Emitted when translations are created.

#### Payload

```ts
{
  id, // The ID of the translation
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createTranslationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTranslationsWorkflow/index.html.md)
- [batchTranslationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchTranslationsWorkflow/index.html.md)

***

### translation.updated&#xA;

Emitted when translations are updated.

#### Payload

```ts
{
  id, // The ID of the translation
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateTranslationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTranslationsWorkflow/index.html.md)
- [batchTranslationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchTranslationsWorkflow/index.html.md)

***

### translation.deleted&#xA;

Emitted when translations are deleted.

#### Payload

```ts
{
  id, // The ID of the translation
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteTranslationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTranslationsWorkflow/index.html.md)
- [batchTranslationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchTranslationsWorkflow/index.html.md)

***

## User Events

### Summary

|Event|Description|
|---|---|
|user.created|Emitted when users are created.|
|user.updated|Emitted when users are updated.|
|user.deleted|Emitted when users are deleted.|

### user.created

Emitted when users are created.

#### Payload

```ts
{
  id, // The ID of the user
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md)
- [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md)
- [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md)

***

### user.updated

Emitted when users are updated.

#### Payload

```ts
{
  id, // The ID of the user
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateUsersWorkflow/index.html.md)

***

### user.deleted

Emitted when users are deleted.

#### Payload

```ts
{
  id, // The ID of the user
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md)
- [removeUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeUserAccountWorkflow/index.html.md)


# build Command - Medusa CLI Reference

Create a standalone build of the Medusa application that you can deploy to production.

This creates a build that:

- Doesn't rely on the source TypeScript files.
- Can be copied to a production server reliably.

The build is output to a new `.medusa/server` directory.

```bash
npx medusa build
```

Refer to the [Build Medusa Application](https://docs.medusajs.com/docs/learn/build/index.html.md) guide for next steps.

## Options

|Option|Description|
|---|---|---|
|\`--admin-only\`|Whether to build only the admin to host it separately. If this option is not passed, the admin is built to the |

***

## Build Medusa Admin

By default, the Medusa Admin is built to the `.medusa/server/public/admin` directory.

If you want a separate build to host the admin as a standalone application, such as on Vercel, pass the `--admin-only` option as explained in the [Options](#options) section. This outputs the admin to the `.medusa/admin` directory instead.


# db Commands - Medusa CLI Reference

Commands in the Medusa CLI starting with `db:` perform actions on the database.

## db:setup

Creates a database for the Medusa application with the specified name, if it doesn't exist. Then, it runs migrations and syncs links.

It also updates your `.env` file with the database name.

```bash
npx medusa db:setup --db <name>
```

Use this command if you're setting up a Medusa project or database manually.

### Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`--db \<name>\`|The database name.|Yes|-|
|\`--skip-links\`|Skip syncing links to the database.|No|Links are synced by default.|
|\`--execute-safe-links\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.|
|\`--execute-all-links\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.|
|\`--no-interactive\`|Disable the command's prompts.|No|-|

***

## db:create

Creates a database for the Medusa application with the specified name, if it doesn't exist.

It also updates your `.env` file with the database name.

```bash
npx medusa db:create --db <name>
```

Use this command if you want to only create a database.

### Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`--db \<name>\`|The database name.|Yes|-|
|\`--no-interactive\`|Disable the command's prompts.|No|-|

***

## db:generate

Generate a migration file for the latest changes in one or more modules.

```bash
npx medusa db:generate <module_names...>
```

### Arguments

|Argument|Description|Required|
|---|---|---|---|---|
|\`module\_names\`|The name of one or more modules (separated by spaces) to generate migrations for. For example, |Yes|

***

## db:migrate

Run the latest migrations to reflect changes on the database, sync link definitions with the database, and run migration data scripts.

```bash
npx medusa db:migrate
```

Use this command if you've updated the Medusa packages, or you've created customizations and want to reflect them in the database.

### Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`--skip-links\`|Skip syncing links to the database.|No|Links are synced by default.|
|\`--skip-scripts\`|Skip running data migration scripts. This option is added starting from
|No|Data migration scripts are run by default starting from
|
|\`--execute-safe-links\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.|
|\`--execute-all-links\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.|

***

## db:rollback

Revert the last migrations run on one or more modules.

```bash
npx medusa db:rollback <module_names...>
```

### Arguments

|Argument|Description|Required|
|---|---|---|---|---|
|\`module\_names\`|The name of one or more modules (separated by spaces) to rollback their migrations for. For example, |Yes|

***

## db:sync-links

Sync the database with the link definitions in your application, including the definitions in Medusa's modules.

```bash
npx medusa db:sync-links
```

### Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`--execute-safe\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.|
|\`--execute-all\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.|


# develop Command - Medusa CLI Reference

Start the Medusa application in development.

This command watches files for any changes, then rebuilds the files and restarts the Medusa application.

```bash
npx medusa develop
```

## Options

|Option|Description|Default|
|---|---|---|---|---|
|\`-H \<host>\`|Set the host of the Medusa server.|\`localhost\`|
|\`-p \<port>\`|Set the port of the Medusa server.|\`9000\`|


# exec Command - Medusa CLI Reference

Run a custom CLI script using Medusa's CLI tool. Learn more about it in [Custom CLI Scripts guide](https://docs.medusajs.com/docs/learn/fundamentals/custom-cli-scripts/index.html.md).

```bash
npx medusa exec [file] [args...]
```

## Arguments

|Argument|Description|Required|
|---|---|---|---|---|
|\`file\`|The path to the TypeScript or JavaScript file containing the function to execute.|Yes|
|\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No|


# new Command - Medusa CLI Reference

Create a new Medusa application. Unlike the `create-medusa-app` CLI tool, this command provides more flexibility for experienced Medusa developers in creating and configuring their project.

```bash
medusa new [<dir_name> [<starter_url>]]
```

## Arguments

|Argument|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`dir\_name\`|The name of the directory to create the Medusa application in.|Yes|-|
|\`starter\_url\`|The URL of the starter repository to create the project from.|No|\`https://github.com/medusajs/medusa-starter-default\`|

## Options

|Option|Description|
|---|---|---|
|\`-y\`|Skip all prompts, such as database prompts. A database might not be created if default PostgreSQL credentials don't work.|
|\`--skip-db\`|Skip database creation.|
|\`--skip-env\`|Skip populating |
|\`--db-user \<user>\`|The database user to use for database setup.|
|\`--db-database \<database>\`|The name of the database used for database setup.|
|\`--db-pass \<password>\`|The database password to use for database setup.|
|\`--db-port \<port>\`|The database port to use for database setup.|
|\`--db-host \<host>\`|The database host to use for database setup.|


# plugin Commands - Medusa CLI Reference

Commands in the Medusa CLI starting with `plugin:` perform actions related to [plugin development](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md).

These commands are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0).

## plugin:publish

Publish a plugin into the local packages registry. The command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. You can then install the plugin in a local Medusa project using the [plugin:add](#pluginadd) command.

```bash
npx medusa plugin:publish
```

***

## plugin:add

Install the specified plugins from the local package registry into a local Medusa application. Plugins can be added to the local package registry using the [plugin:publish](#pluginpublish) command.

```bash
npx medusa plugin:add [names...]
```

### Arguments

|Argument|Description|Required|
|---|---|---|---|---|
|\`names\`|The names of one or more plugins to install from the local package registry. A plugin's name is as specified in its |Yes|

***

## plugin:develop

Start a development server for a plugin. The command will watch for changes in the plugin's source code and automatically re-publish the changes into the local package registry.

```bash
npx medusa plugin:develop
```

***

## plugin:db:generate

Generate migrations for all modules in a plugin.

```bash
npx medusa plugin:db:generate
```

***

## plugin:build

Build a plugin before publishing it to NPM. The command will compile the output in the `.medusa/server` directory.

```bash
npx medusa plugin:build
```


# start Command - Medusa CLI Reference

Start the Medusa application in production.

```bash
npx medusa start
```

## Options

|Option|Description|Default|
|---|---|---|---|---|
|\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`|
|\`-p \<port>\`|Set port of the Medusa server.|\`9000\`|
|\`--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

Enable or disable the collection of anonymous usage data. If no option is provided, the command enables the collection of anonymous usage data.

```bash
npx medusa telemetry
```

#### Options

|Option|Description|
|---|---|---|
|\`--enable\`|Enable telemetry (default).|
|\`--disable\`|Disable telemetry.|


# user Command - Medusa CLI Reference

Create a new admin user.

```bash
npx medusa user --email <email> [--password <password>]
```

## Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`-e \<email>\`|The user's email.|Yes|-|
|\`-p \<password>\`|The user's password.|No|-|
|\`-i \<id>\`|The user's ID.|No|An automatically generated ID is used.|
|\`--invite\`|Whether to create a user invite instead of directly creating a user. Learn more in the |No|\`false\`|

***

## Create User Invite with Medusa CLI

The `user` command accepts the `--invite` option to create a user invite. The user must accept the invite before they can log into the Medusa Admin.

For example:

```bash
npx medusa user --email user@example.com --invite
```

The command will create a user invite and output the invite token. You can then either:

- Accept the invite in the Medusa Admin at the path `/app/invite?token=<invite_token>`
- Accept the invite using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept).


# Medusa CLI Reference

The Medusa CLI tool provides commands that facilitate your development.

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

## Usage

In your Medusa application's directory, you can use the Medusa CLI tool using NPX.

For example:

```bash
npx medusa --help
```

***


# build Command - Medusa CLI Reference

Create a standalone build of the Medusa application that you can deploy to production.

This creates a build that:

- Doesn't rely on the source TypeScript files.
- Can be copied to a production server reliably.

The build is output to a new `.medusa/server` directory.

```bash
npx medusa build
```

Refer to the [Build Medusa Application](https://docs.medusajs.com/docs/learn/build/index.html.md) guide for next steps.

## Options

|Option|Description|
|---|---|---|
|\`--admin-only\`|Whether to build only the admin to host it separately. If this option is not passed, the admin is built to the |

***

## Build Medusa Admin

By default, the Medusa Admin is built to the `.medusa/server/public/admin` directory.

If you want a separate build to host the admin as a standalone application, such as on Vercel, pass the `--admin-only` option as explained in the [Options](#options) section. This outputs the admin to the `.medusa/admin` directory instead.


# db Commands - Medusa CLI Reference

Commands in the Medusa CLI starting with `db:` perform actions on the database.

## db:setup

Creates a database for the Medusa application with the specified name, if it doesn't exist. Then, it runs migrations and syncs links.

It also updates your `.env` file with the database name.

```bash
npx medusa db:setup --db <name>
```

Use this command if you're setting up a Medusa project or database manually.

### Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`--db \<name>\`|The database name.|Yes|-|
|\`--skip-links\`|Skip syncing links to the database.|No|Links are synced by default.|
|\`--execute-safe-links\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.|
|\`--execute-all-links\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.|
|\`--no-interactive\`|Disable the command's prompts.|No|-|

***

## db:create

Creates a database for the Medusa application with the specified name, if it doesn't exist.

It also updates your `.env` file with the database name.

```bash
npx medusa db:create --db <name>
```

Use this command if you want to only create a database.

### Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`--db \<name>\`|The database name.|Yes|-|
|\`--no-interactive\`|Disable the command's prompts.|No|-|

***

## db:generate

Generate a migration file for the latest changes in one or more modules.

```bash
npx medusa db:generate <module_names...>
```

### Arguments

|Argument|Description|Required|
|---|---|---|---|---|
|\`module\_names\`|The name of one or more modules (separated by spaces) to generate migrations for. For example, |Yes|

***

## db:migrate

Run the latest migrations to reflect changes on the database, sync link definitions with the database, and run migration data scripts.

```bash
npx medusa db:migrate
```

Use this command if you've updated the Medusa packages, or you've created customizations and want to reflect them in the database.

### Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`--skip-links\`|Skip syncing links to the database.|No|Links are synced by default.|
|\`--skip-scripts\`|Skip running data migration scripts. This option is added starting from
|No|Data migration scripts are run by default starting from
|
|\`--execute-safe-links\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.|
|\`--execute-all-links\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.|

***

## db:rollback

Revert the last migrations run on one or more modules.

```bash
npx medusa db:rollback <module_names...>
```

### Arguments

|Argument|Description|Required|
|---|---|---|---|---|
|\`module\_names\`|The name of one or more modules (separated by spaces) to rollback their migrations for. For example, |Yes|

***

## db:sync-links

Sync the database with the link definitions in your application, including the definitions in Medusa's modules.

```bash
npx medusa db:sync-links
```

### Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`--execute-safe\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.|
|\`--execute-all\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.|


# develop Command - Medusa CLI Reference

Start the Medusa application in development.

This command watches files for any changes, then rebuilds the files and restarts the Medusa application.

```bash
npx medusa develop
```

## Options

|Option|Description|Default|
|---|---|---|---|---|
|\`-H \<host>\`|Set the host of the Medusa server.|\`localhost\`|
|\`-p \<port>\`|Set the port of the Medusa server.|\`9000\`|


# exec Command - Medusa CLI Reference

Run a custom CLI script using Medusa's CLI tool. Learn more about it in [Custom CLI Scripts guide](https://docs.medusajs.com/docs/learn/fundamentals/custom-cli-scripts/index.html.md).

```bash
npx medusa exec [file] [args...]
```

## Arguments

|Argument|Description|Required|
|---|---|---|---|---|
|\`file\`|The path to the TypeScript or JavaScript file containing the function to execute.|Yes|
|\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No|


# new Command - Medusa CLI Reference

Create a new Medusa application. Unlike the `create-medusa-app` CLI tool, this command provides more flexibility for experienced Medusa developers in creating and configuring their project.

```bash
medusa new [<dir_name> [<starter_url>]]
```

## Arguments

|Argument|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`dir\_name\`|The name of the directory to create the Medusa application in.|Yes|-|
|\`starter\_url\`|The URL of the starter repository to create the project from.|No|\`https://github.com/medusajs/medusa-starter-default\`|

## Options

|Option|Description|
|---|---|---|
|\`-y\`|Skip all prompts, such as database prompts. A database might not be created if default PostgreSQL credentials don't work.|
|\`--skip-db\`|Skip database creation.|
|\`--skip-env\`|Skip populating |
|\`--db-user \<user>\`|The database user to use for database setup.|
|\`--db-database \<database>\`|The name of the database used for database setup.|
|\`--db-pass \<password>\`|The database password to use for database setup.|
|\`--db-port \<port>\`|The database port to use for database setup.|
|\`--db-host \<host>\`|The database host to use for database setup.|


# plugin Commands - Medusa CLI Reference

Commands in the Medusa CLI starting with `plugin:` perform actions related to [plugin development](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md).

These commands are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0).

## plugin:publish

Publish a plugin into the local packages registry. The command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. You can then install the plugin in a local Medusa project using the [plugin:add](#pluginadd) command.

```bash
npx medusa plugin:publish
```

***

## plugin:add

Install the specified plugins from the local package registry into a local Medusa application. Plugins can be added to the local package registry using the [plugin:publish](#pluginpublish) command.

```bash
npx medusa plugin:add [names...]
```

### Arguments

|Argument|Description|Required|
|---|---|---|---|---|
|\`names\`|The names of one or more plugins to install from the local package registry. A plugin's name is as specified in its |Yes|

***

## plugin:develop

Start a development server for a plugin. The command will watch for changes in the plugin's source code and automatically re-publish the changes into the local package registry.

```bash
npx medusa plugin:develop
```

***

## plugin:db:generate

Generate migrations for all modules in a plugin.

```bash
npx medusa plugin:db:generate
```

***

## plugin:build

Build a plugin before publishing it to NPM. The command will compile the output in the `.medusa/server` directory.

```bash
npx medusa plugin:build
```


# start Command - Medusa CLI Reference

Start the Medusa application in production.

```bash
npx medusa start
```

## Options

|Option|Description|Default|
|---|---|---|---|---|
|\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`|
|\`-p \<port>\`|Set port of the Medusa server.|\`9000\`|
|\`--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

Enable or disable the collection of anonymous usage data. If no option is provided, the command enables the collection of anonymous usage data.

```bash
npx medusa telemetry
```

#### Options

|Option|Description|
|---|---|---|
|\`--enable\`|Enable telemetry (default).|
|\`--disable\`|Disable telemetry.|


# user Command - Medusa CLI Reference

Create a new admin user.

```bash
npx medusa user --email <email> [--password <password>]
```

## Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`-e \<email>\`|The user's email.|Yes|-|
|\`-p \<password>\`|The user's password.|No|-|
|\`-i \<id>\`|The user's ID.|No|An automatically generated ID is used.|
|\`--invite\`|Whether to create a user invite instead of directly creating a user. Learn more in the |No|\`false\`|

***

## Create User Invite with Medusa CLI

The `user` command accepts the `--invite` option to create a user invite. The user must accept the invite before they can log into the Medusa Admin.

For example:

```bash
npx medusa user --email user@example.com --invite
```

The command will create a user invite and output the invite token. You can then either:

- Accept the invite in the Medusa Admin at the path `/app/invite?token=<invite_token>`
- Accept the invite using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept).


# Medusa CLI Reference

The Medusa CLI tool provides commands that facilitate your development.

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

## Usage

In your Medusa application's directory, you can use the Medusa CLI tool using NPX.

For example:

```bash
npx medusa --help
```

***


# Authentication in JS SDK

In this guide, you'll learn about the default authentication setup when using the JS SDK, how to customize it, and how to send authenticated requests to Medusa's APIs.

## Default Authentication Settings in JS SDK

The JS SDK facilitates authentication by storing and managing the necessary authorization headers or sessions for you.

There are three types of authentication:

|Method|Description|When to use|
|---|---|---|
|JWT token (default)|When you log in a user, the JS SDK stores the JWT for you and automatically includes it in the headers of all requests to the Medusa API. This means you don't have to manually set the authorization header for each request. When the user logs out, the SDK clears the stored JWT.||
|Cookie session|When you log in a user, the JS SDK stores the session cookie for you and automatically includes it in the headers of all requests to the Medusa API. This means you don't have to manually set the authorization header for each request. When the user logs out, the SDK destroys the session cookie using Medusa's API.||
|Secret API Key|Only available for admin users. You pass the API key in the JS SDK configurations, and it's always passed in the headers of all requests to the Medusa API.||

***

## JS SDK Authentication Configurations

The JS SDK provides a set of configurations to customize the authentication method and storage. You can set these configurations when initializing the SDK.

For a full list of JS SDK configurations and their possible values, check out the [JS SDK Overview](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk#js-sdk-configurations/index.html.md) documentation.

### Authentication Type

By default, the JS SDK uses JWT token (`jwt`) authentication. You can change the authentication method or type by setting the `auth.type` configuration to `session`.

For example:

```ts
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  // ...
  auth: {
    type: "session",
  },
})
```

To use a secret API key instead, pass it in the `apiKey` configuration instead:

```ts
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  // ...
  apiKey: "your-api-key",
})
```

The provided API key will be passed in the headers of all requests to the Medusa API.

### Change JWT Authentication Storage

By default, the JS SDK stores the JWT token in the `localStorage` under the `medusa_auth_token` key.

Some environments or use cases may require a different storage method or `localStorage` may not be available. For example, if you're building a mobile app with React Native, you might want to use `AsyncStorage` instead of `localStorage`.

You can change the storage method by setting the `auth.jwtTokenStorageMethod` configuration to one of the following values:

|Value|Description|
|---|---|
|\`local\`|Uses |
|\`session\`|Uses |
|\`memory\`|Uses a memory storage method. This means the token will be cleared when the user refreshes the page or closes the browser tab or window. This is also useful when using the JS SDK in a server-side environment.|
|\`custom\`|Uses a custom storage method. This means you can provide your own implementation of the storage method. For example, you can use |
|\`nostore\`|Does not store the JWT token. This means you have to manually set the authorization header for each request. This is useful when you want to use a different authentication method or when you're using the JS SDK in a server-side environment.|

#### Custom Authentication Storage in JS SDK

To use a custom storage method, you need to set the `auth.jwtTokenStorageMethod` configuration to `custom` and provide your own implementation of the storage method in the `auth.storage` configuration.

The object or class passed to `auth.storage` configuration must have the following methods:

- `setItem`: A function that accepts a key and value to store the JWT token.
- `getItem`: A function that accepts a key to retrieve the JWT token.
- `removeItem`: A function that accepts a key to remove the JWT token from storage.

For example, to use `AsyncStorage` in React Native:

```ts
import AsyncStorage from "@react-native-async-storage/async-storage"
import Medusa from "@medusajs/js-sdk"

let MEDUSA_BACKEND_URL = "http://localhost:9000"

if (process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL) {
  MEDUSA_BACKEND_URL = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL
}

export const sdk = new Medusa({
  baseUrl: MEDUSA_BACKEND_URL,
  debug: process.env.NODE_ENV === "development",
  publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
  auth: {
    type: "jwt",
    jwtTokenStorageMethod: "custom",
    storage: AsyncStorage,
  },
})
```

In this example, you specify the `jwtTokenStorageMethod` as `custom` and set the `storage` configuration to `AsyncStorage`. This way, the JS SDK will use `AsyncStorage` to store and manage the JWT token instead of `localStorage`.

### Change Cookie Session Credentials Options

By default, if you set the `auth.type` configuration in the JS SDK to `session`, the JS SDK will pass the `credentials: include` option in the underlying [fetch requests](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#including_credentials).

However, some platforms or environments may not support passing this option. For example, if you're using the JS SDK in a server-side environment or a mobile app, you might want to set the `credentials` option to `same-origin` or `omit`.

You can change the `credentials` option by setting the `auth.fetchCredentials` configuration to one of the following values:

|Value|Description|
|---|---|
|\`include\`|Passes the |
|\`same-origin\`|Passes the |
|\`omit\`|Passes the |

For example:

```ts
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  // ...
  auth: {
    type: "session",
    fetchCredentials: "same-origin",
  },
})
```

In this example, you set the `fetchCredentials` configuration to `same-origin`, which means the JS SDK will include cookies and authorization headers in the requests to the Medusa API only if the request is made to the same origin as the current page.

***

## Sending Authenticated Requests in JS SDK

If you're using an API key for authentication, you don't need to log in the user.

The JS SDK has an `auth.login` method that allows you to login admin users, customers, or any [actor type](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-identity-and-actor-types/index.html.md) with any [auth provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/index.html.md).

Not only does this method log in the user, but it also stores the JWT token or session cookie for you and automatically includes it in the headers of all requests to the Medusa API. This means you don't have to manually set the authorization header for each request.

For example:

### Admin User

```ts
sdk.auth.login("user", "emailpass", {
  email,
  password,
})
.then((data) => {
  if (typeof data === "object" && data.location){
    // authentication requires more actions
  }
  // user is authenticated
})
.catch((error) => {
  // authentication failed
})
```

### Customer

```ts
sdk.auth.login("customer", "emailpass", {
  email,
  password,
})
.then((data) => {
  if (typeof data === "object" && data.location){
    // authentication requires more actions
  }
  // customer is authenticated
})
.catch((error) => {
  // authentication failed
})
```

### Custom

```ts
sdk.auth.login("manager", "emailpass", {
  email,
  password,
})
.then((data) => {
  if (typeof data === "object" && data.location){
    // authentication requires more actions
  }
  // manager is authenticated
})
.catch((error) => {
  // authentication failed
})
```

In this example, you call the `sdk.auth.login` method passing it the actor type (for example, `user`), the provider (`emailpass`), and the credentials.

If the authentication is successful, there are two types of returned data:

- An object with a `location` property: This means the authentication requires more actions, which happens when using third-party authentication providers, such as [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md). In that case, you need to redirect the customer to the location to complete their authentication.
  - Refer to the [Third-Party Login in Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md) guide for an example implementation.
- A string: This means the authentication was successful, and the user is logged in. The JS SDK automatically stores the JWT token or session cookie for you and includes it in the headers of all requests to the Medusa API. All requests you send afterwards will be authenticated with the stored token or session cookie.

If the authentication fails, the `catch` block will be executed, and you can handle the error accordingly.

You can learn more about this method in the [auth.login reference](https://docs.medusajs.com/references/js-sdk/auth/login/index.html.md).

### Manually Set JWT Token

If you need to set the JWT token manually, you can use the `sdk.client.setToken` method. All subsequent requests will be authenticated with the provided token.

For example:

```ts
sdk.client.setToken("your-jwt-token")

// all requests sent after this will be authenticated with the provided token
```

You can also clear the token manually as explained in the [Manually Clearing JWT Token](#manually-clearing-jwt-token) section.

***

## Logout in JS SDK

If you're using an API key for authentication, you can't log out the user. You'll have to unset the API key in the JS SDK configurations.

The JS SDK has an `auth.logout` method that allows you to log out the currently authenticated user.

If the JS SDK's authentication type is `jwt`, the method will only clear the stored JWT token from the local storage. If the authentication type is `session`, the method will destroy the session cookie using Medusa's `/auth/session` API route.

Any request sent after logging out will not be authenticated, and you will need to log in again to authenticate the user.

For example:

```ts
sdk.auth.logout()
.then(() => {
  // user is logged out
})
```

You can learn more about this method in the [auth.logout reference](https://docs.medusajs.com/references/js-sdk/auth/logout/index.html.md).

### Manually Clearing JWT Token

If you need to clear the JWT token manually, you can use the `sdk.client.clearToken` method. This will remove the token from the local storage and all subsequent requests will not be authenticated.

For example:

```ts
sdk.client.clearToken()

// all requests sent after this will not be authenticated
```


# Medusa JS SDK

In this documentation, you'll learn how to install and use Medusa's JS SDK.

## What is Medusa JS SDK?

Medusa's JS SDK is a library to easily send requests to your Medusa application. You can use it in your admin customizations or custom storefronts.

***

## How to Install Medusa JS SDK?

The Medusa JS SDK is available in your Medusa application by default. So, you don't need to install it before using it in your admin customizations.

To install the Medusa JS SDK in other projects, such as a custom storefront, run the following command:

```bash npm2yarn
npm install @medusajs/js-sdk@latest @medusajs/types@latest
```

You install two libraries:

- `@medusajs/js-sdk`: the Medusa JS SDK.
- `@medusajs/types`: Medusa's types library, which is useful if you're using TypeScript in your development.

***

## Setup JS SDK

In your project, create the following `config.ts` file:

For admin customizations, create this file at `src/admin/lib/config.ts`.

### Admin (Medusa project)

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

### Admin (Medusa Plugin)

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: __BACKEND_URL__ || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

### Storefront

```ts title="sdk.ts"
import Medusa from "@medusajs/js-sdk"

let MEDUSA_BACKEND_URL = "http://localhost:9000"

if (process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL) {
  MEDUSA_BACKEND_URL = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL
}

export const sdk = new Medusa({
  baseUrl: MEDUSA_BACKEND_URL,
  debug: process.env.NODE_ENV === "development",
  publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
})
```

In Medusa Admin customizations that are created in a Medusa project, you use `import.meta.env` to access environment variables, whereas in customizations built in a Medusa plugin, you use the global variable `__BACKEND_URL__` to access the backend URL. You can learn more in the [Admin Environment Variables](https://docs.medusajs.com/docs/learn/fundamentals/admin/environment-variables/index.html.md) chapter.

### JS SDK Configurations

The `Medusa` initializer accepts as a parameter an object with the following properties:

|Property|Description|Default|
|---|---|---|---|---|
|\`baseUrl\`|A required string indicating the URL to the Medusa backend.|-|
|\`publishableKey\`|A string indicating the publishable API key to use in the storefront. You can retrieve it from the Medusa Admin.|-|
|\`auth.type\`|A string that specifies the user authentication method to use.|-|
|\`auth.jwtTokenStorageKey\`|A string that, when |\`medusa\_auth\_token\`|
|\`auth.jwtTokenStorageMethod\`|A string that, when |\`local\`|
|\`auth.storage\`|This option is only available after Medusa v2.5.1. It's an object or class that's used when |-|
|\`auth.fetchCredentials\`|By default, if |\`include\`|
|\`globalHeaders\`|An object of key-value pairs indicating headers to pass in all requests, where the key indicates the name of the header field.|-|
|\`apiKey\`|A string indicating the admin user's API key. If specified, it's used to send authenticated requests.|-|
|\`debug\`|A boolean indicating whether to show debug messages of requests sent in the console. This is useful during development.|\`false\`|
|\`logger\`|Replace the logger used by the JS SDK to log messages. The logger must be a class or object having the following methods:|JavaScript's |

***

## Manage Authentication in JS SDK

The JS SDK supports different types of authentication methods and allow you to flexibly configure them.

To learn more about configuring authentication in the JS SDK and sending authenticated requests, refer to the [Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/auth/overview/index.html.md) guide.

***

## Send Requests to Custom Routes

The sidebar shows the different methods that you can use to send requests to Medusa's API routes.

To send requests to custom routes, the JS SDK has a `client.fetch` method that wraps the [JavaScript Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) that you can use. The method automatically appends configurations and headers, such as authentication headers, to your request.

For example, to send a request to a custom route at `http://localhost:9000/custom`:

### GET

```ts
sdk.client.fetch(`/custom`)
.then((data) => {
  console.log(data)
})
```

### POST

```ts
sdk.client.fetch(`/custom`, {
  method: "post",
  body: {
    id: "123",
  },
}).then((data) => {
  console.log(data)
})
```

### DELETE

```ts
sdk.client.fetch(`/custom`, {
  method: "delete",
}).then(() => {
  console.log("success")
})
```

The `fetch` method accepts as a first parameter the route's path relative to the `baseUrl` configuration you passed when you initialized the SDK.

In the second parameter, you can pass an object of [request configurations](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit). You don't need to configure the content-type to be JSON, or stringify the `body` or `query` value, as that's handled by the method.

The method returns a Promise that, when resolved, has the data returned by the request. If the request returns a JSON object, it'll be automatically parsed to a JavaScript object and returned.

***

## Stream Server-Sent Events

The JS SDK supports streaming server-sent events (SSE) using the `client.fetchStream` method. This method is useful when you want to receive real-time updates from the server.

For example, consider you have the following custom API route at `src/api/admin/stream/route.ts`:

```ts title="src/api/admin/stream/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  })

  const interval = setInterval(() => {
    res.write("data: Streaming data...\n\n")
  }, 3000)

  req.on("close", () => {
    clearInterval(interval)
    res.end()
  })
  
  req.on("end", () => {
    clearInterval(interval)
    res.end()
  })
}
```

Then, you can use the `client.fetchStream` method in a UI route to receive the streaming data:

```tsx title="src/admin/route/stream/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading, Button, Text } from "@medusajs/ui"
import { useState } from "react"
import { sdk } from "../../lib/sdk"

const StreamTestPage = () => {
  const [messages, setMessages] = useState<string[]>([])
  const [isStreaming, setIsStreaming] = useState(false)
  const [abortStream, setAbortStream] = useState<(() => void) | null>(null)

  const startStream = async () => {
    setIsStreaming(true)
    setMessages([])
    
    const { stream, abort } = await sdk.client.fetchStream("/admin/stream")

    if (!stream) {
      console.error("Failed to start stream")
      setIsStreaming(false)
      return
    }

    // Store the abort function for the abort button
    setAbortStream(() => abort)

    try {
      for await (const chunk of stream) {
        // Since the server sends plain text, convert to string
        const message = typeof chunk === "string" ? chunk : (chunk.data || String(chunk))
        setMessages((prev) => [...prev, message.trim()])
      }
    } catch (error) {
      // Don't log abort errors as they're expected when user clicks abort
      if (error instanceof Error && error.name !== "AbortError") {
        console.error("Stream error:", error)
      }
    } finally {
      setIsStreaming(false)
      setAbortStream(null)
    }
  }

  const handleAbort = () => {
    if (abortStream) {
      abortStream()
      setIsStreaming(false)
      setAbortStream(null)
    }
  }

  return (
    <Container className="p-6">
      <Heading level="h1" className="mb-6">
        fetchStream Example
      </Heading>
      
      <div className="space-y-4">
        <div className="flex gap-2">
          <Button 
            onClick={startStream} 
            disabled={isStreaming}
            variant="primary"
          >
            {isStreaming ? "Streaming..." : "Start Stream"}
          </Button>
          
          <Button 
            onClick={handleAbort} 
            disabled={!isStreaming}
            variant="secondary"
          >
            Abort Stream
          </Button>
        </div>
        
        <div className="border rounded p-4 h-64 overflow-y-auto bg-ui-bg-subtle">
          {messages.length === 0 ? (
            <Text className="text-ui-fg-muted">No messages yet...</Text>
          ) : (
            messages.map((msg, index) => (
              <div key={index} className="mb-2 text-sm">
                {msg}
              </div>
            ))
          )}
        </div>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Stream Test",
})

export default StreamTestPage
```

`fetchStream` accepts the same parameters as `fetch`, but it returns an object having two properties:

- `stream`: An [AsyncGenerator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator) that you can use to iterate over the streaming data.
- `abort`: A function that you can call to abort the stream. This is useful when you want to stop receiving data from the server.

In this example, when the user clicks the "Start Stream" button, you start the stream and listen for incoming data. The data is received as chunks, which you can process and display in the UI.

***

## Handle Errors

If an error occurs in a request, the JS SDK throws a `FetchError` object. This object has the following properties:

- `status`: The HTTP status code of the response.
- `statusText`: The error code. For example, `Unauthorized`.
- `message`: The error message. For example, `Invalid credentials`.

You can use these properties to handle errors in your application.

For example:

### Promise

```ts
sdk.store.customer.listAddress()
.then(({ addresses, count, offset, limit }) => {
  // no errors occurred
  // do something with the data
  console.log(addresses)
})
.catch((error) => {
  const fetchError = error as FetchError

  if (fetchError.statusText === "Unauthorized") {
    // redirect to login page
  } else {
    // handle other errors
  }
})
```

### Async/Await

```ts
try {
  const { 
    addresses, 
    count, 
    offset, 
    limit,
  } = await sdk.store.customer.listAddress()
  // no errors occurred
  // do something with the data
  console.log(addresses)
} catch (error) {
  const fetchError = error as FetchError

  if (fetchError.statusText === "Unauthorized") {
    // redirect to login page
  } else {
    // handle other errors
  }
}
```

In the example above, you handle errors in two ways:

- Since the JS SDK's methods return a Promise, you can use the `catch` method to handle errors.
- You can use the `try...catch` statement to handle errors when using `async/await`. This is useful when you're executing the methods as part of a larger function.

In the `catch` method or statement, you have access to the error object of type `FetchError`.

An example of handling the error is to check if the error's `statusText` is `Unauthorized`. If so, you can redirect the customer to the login page. Otherwise, you can handle other errors by showing an alert, for example.

***

## Pass Headers in Requests

There are two ways to pass custom headers in requests when using the JS SDK:

1. Using the `globalHeaders` configuration: This is useful when you want to pass the same headers in all requests. For example, if you want to pass a custom header for tracking purposes:

```ts
const sdk = new Medusa({
  // ...
  globalHeaders: {
    "x-tracking-id": "123456789",
  },
})
```

2. Using the headers parameter of a specific method. Every method has as a last parameter a headers parameter, which is an object of headers to pass in the request. This is useful when you want to pass a custom header in specific requests. For example, to disable HTTP compression for specific requests:

```ts
sdk.store.product.list({
  limit,
  offset,
}, {
  "x-no-compression": "false",
})
```

In the example above, you pass the `x-no-compression` header in the request to disable HTTP compression. You pass it as the last parameter of the `sdk.store.product.list` method.

The JS SDK appends request-specific headers to authentication headers and headers configured in the `globalHeaders` configuration. So, in the example above, the `x-no-compression` header is passed in the request along with the authentication headers and any headers configured in the `globalHeaders` configuration.

***

## Localization with JS SDK

### Prerequisites

- [Translation Module Configured](https://docs.medusajs.com/commerce-modules/translation#configure-translation-module/index.html.md)

If you support [localization](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/localization/index.html.md) in your storefront, you can use the `setLocale` method of the JS SDK.

The `setLocale` method stores the locale in the JS SDK to pass the it as a `x-medusa-locale` header in all subsequent requests. Also, if your application supports `localStorage`, the method sets the locale in the `medusa_locale` key of the `localStorage`.

For example, to set the locale to French (France):

```ts
sdk.setLocale("fr-FR")
// products content is fetched in French
const { products } = await sdk.store.product.list()
```

The `setLocale` method accepts the locale string in the [IETF BCP 47 standard](https://gist.github.com/typpo/b2b828a35e683b9bf8db91b5404f1bd1). All subsequent requests sent using the JS SDK will include the specified locale in the `x-medusa-locale` header.

In the above example, the product content, such as title and description, is fetched in French and falls back to the original content if translations aren't available.

You can also retrieve the currently set locale using the `getLocale` method:

```ts
const currentLocale = sdk.getLocale()
console.log(currentLocale) // "fr-FR"
```

Learn more about localization and translations in the [Translation Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/translation/index.html.md) guide.

***

## Medusa JS SDK Tips

### Use Tanstack (React) Query in Admin Customizations

In admin customizations, use [Tanstack Query](https://tanstack.com/query/latest) with the JS SDK to send requests to custom or existing API routes.

Tanstack Query is installed by default in your Medusa application.

Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency.

Use the [configured SDK](#setup-js-sdk) with the [useQuery](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery#usequery) Tanstack Query hook to send `GET` requests, and [useMutation](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation#usemutation) hook to send `POST` or `DELETE` requests.

For example:

### Query

```tsx title="src/admin/widgets/product-widget.ts"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Button, Container } from "@medusajs/ui"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/config"
import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types"

const ProductWidget = () => {
  const { data, isLoading } = useQuery({
    queryFn: () => sdk.admin.product.list(),
    queryKey: ["products"],
  })
  
  return (
    <Container className="divide-y p-0">
      {isLoading && <span>Loading...</span>}
      {data?.products && (
        <ul>
          {data.products.map((product) => (
            <li key={product.id}>{product.title}</li>
          ))}
        </ul>
      )}
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.list.before",
})

export default ProductWidget
```

### Mutation

```tsx title="src/admin/widgets/product-widget.ts"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Button, Container } from "@medusajs/ui"
import { useMutation } from "@tanstack/react-query"
import { sdk } from "../lib/config"
import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types"

const ProductWidget = ({ 
  data: productData,
}: DetailWidgetProps<HttpTypes.AdminProduct>) => {
  const { mutateAsync } = useMutation({
    mutationFn: (payload: HttpTypes.AdminUpdateProduct) => 
      sdk.admin.product.update(productData.id, payload),
    onSuccess: () => alert("updated product"),
  })

  const handleUpdate = () => {
    mutateAsync({
      title: "New Product Title",
    })
  }
  
  return (
    <Container className="divide-y p-0">
      <Button onClick={handleUpdate}>Update Title</Button>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

Refer to Tanstack Query's documentation to learn more about sending [Queries](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery#usequery) and [Mutations](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation#usemutation).

### Cache in Next.js Projects

Every method of the SDK that sends requests accepts as a last parameter an object of key-value headers to pass in the request.

In Next.js storefronts or projects, pass the `next.tags` header in the last parameter for data caching.

For example:

```ts highlights={[["2", "next"], ["3", "tags", "An array of tags to cache the data under."]]}
sdk.store.product.list({}, {
  next: {
    tags: ["products"],
  },
})
```

The `tags` property accepts an array of tags that the data is cached under.

Then, to purge the cache later, use Next.js's `revalidateTag` utility:

```ts
import { revalidateTag } from "next/cache"

// ...

revalidateTag("products")
```

Learn more in the [Next.js documentation](https://nextjs.org/docs/app/building-your-application/caching#fetch-optionsnexttags-and-revalidatetag).


# Implement Custom Line Item Pricing in Medusa

In this guide, you'll learn how to add line items with custom prices to a cart in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) which are available out-of-the-box. These features include managing carts and adding line items to them.

By default, you can add product variants to the cart, where the price of its associated line item is based on the product variant's price. However, you can build customizations to add line items with custom prices to the cart. This is useful when integrating an Enterprise Resource Planning (ERP), Product Information Management (PIM), or other third-party services that provide real-time prices for your products.

To showcase how to add line items with custom prices to the cart, this guide uses [GoldAPI.io](https://www.goldapi.io) as an example of a third-party system that you can integrate for real-time prices. You can follow the same approach for other third-party integrations that provide custom pricing.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer.

### Summary

This guide will teach you how to:

- Install and set up Medusa.
- Integrate the third-party service [GoldAPI.io](https://www.goldapi.io) that retrieves real-time prices for metals like Gold and Silver.
- Add an API route to add a product variant that has metals, such as a gold ring, to the cart with the real-time price retrieved from the third-party service.

![Diagram showcasing overview of implementation for adding an item to cart from storefront.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738920014/Medusa%20Resources/custom-line-item-3_zu3qh2.jpg)

- [Custom Item Price Repository](https://github.com/medusajs/examples/tree/main/custom-item-price): Find the full code for this guide in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1738246728/OpenApi/Custom_Item_Price_gdfnl3.yaml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. You can also optionally choose to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Integrate GoldAPI.io

### Prerequisites

- [GoldAPI.io Account. You can create a free account.](https://www.goldapi.io)

To integrate third-party services into Medusa, you create a custom module. A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

In this step, you'll create a Metal Price Module that uses the GoldAPI.io service to retrieve real-time prices for metals like Gold and Silver. You'll use this module later to retrieve the real-time price of a product variant based on the metals in it, and add it to the cart with that custom price.

Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).

### Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/metal-prices`.

![Diagram showcasing the module directory to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1738247192/Medusa%20Resources/custom-item-price-1_q16evr.jpg)

### Create Module's Service

You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, which is useful if your module defines tables in the database, or connect to a third-party service.

In this section, you'll create the Metal Prices Module's service that connects to the GoldAPI.io service to retrieve real-time prices for metals.

Start by creating the file `src/modules/metal-prices/service.ts` with the following content:

![Diagram showcasing the service file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1738247303/Medusa%20Resources/custom-item-price-2_eaefis.jpg)

```ts title="src/modules/metal-prices/service.ts"
type Options = {
  accessToken: string
  sandbox?: boolean
}

export default class MetalPricesModuleService {
  protected options_: Options

  constructor({}, options: Options) {
    this.options_ = options
  }
}
```

A module can accept options that are passed to its service. You define an `Options` type that indicates the options the module accepts. It accepts two options:

- `accessToken`: The access token for the GoldAPI.io service.
- `sandbox`: A boolean that indicates whether to simulate sending requests to the GoldAPI.io service. This is useful when running in a test environment.

The service's constructor receives the module's options as a second parameter. You store the options in the service's `options_` property.

A module has a container of Medusa Framework tools and local resources in the module that you can access in the service constructor's first parameter. Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md).

#### Add Method to Retrieve Metal Prices

Next, you'll add the method to retrieve the metal prices from the third-party service.

First, add the following types at the beginning of `src/modules/metal-prices/service.ts`:

```ts title="src/modules/metal-prices/service.ts"
export enum MetalSymbols {
  Gold = "XAU",
  Silver = "XAG",
  Platinum = "XPT",
  Palladium = "XPD"
}

export type PriceResponse = {
  metal: MetalSymbols
  currency: string
  exchange: string
  symbol: string
  price: number
  [key: string]: unknown
}

```

The `MetalSymbols` enum defines the symbols for metals like Gold, Silver, Platinum, and Palladium. The `PriceResponse` type defines the structure of the response from the GoldAPI.io's endpoint.

Next, add the method `getMetalPrices` to the `MetalPricesModuleService` class:

```ts title="src/modules/metal-prices/service.ts"
import { MedusaError } from "@medusajs/framework/utils"

// ...

export default class MetalPricesModuleService {
  // ...
  async getMetalPrice(
    symbol: MetalSymbols, 
    currency: string
  ): Promise<PriceResponse> {
    const upperCaseSymbol = symbol.toUpperCase()
    const upperCaseCurrency = currency.toUpperCase()

    return fetch(`https://www.goldapi.io/api/${upperCaseSymbol}/${upperCaseCurrency}`, {
      headers: {
        "x-access-token": this.options_.accessToken,
        "Content-Type": "application/json",
      },
      redirect: "follow",
    }).then((response) => response.json())
    .then((response) => {
      if (response.error) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          response.error
        )
      }

      return response
    })
  }
}
```

The `getMetalPrice` method accepts the metal symbol and currency as parameters. You send a request to GoldAPI.io's `/api/{symbol}/{currency}` endpoint to retrieve the metal's price, also passing the access token in the request's headers.

If the response contains an error, you throw a `MedusaError` with the error message. Otherwise, you return the response, which is of type `PriceResponse`.

#### Add Helper Methods

You'll also add two helper methods to the `MetalPricesModuleService`. The first one is `getMetalSymbols` that returns the metal symbols as an array of strings:

```ts title="src/modules/metal-prices/service.ts"
export default class MetalPricesModuleService {
  // ...
  async getMetalSymbols(): Promise<string[]> {
    return Object.values(MetalSymbols)
  }
}
```

The second is `getMetalSymbol` that receives a name like `gold` and returns the corresponding metal symbol:

```ts title="src/modules/metal-prices/service.ts"
export default class MetalPricesModuleService {
  // ...
  async getMetalSymbol(name: string): Promise<MetalSymbols | undefined> {
    const formattedName = name.charAt(0).toUpperCase() + name.slice(1).toLowerCase()
    return MetalSymbols[formattedName as keyof typeof MetalSymbols]
  }
}
```

You'll use these methods in later steps.

### Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/metal-prices/index.ts` with the following content:

![The directory structure of the Metal Prices Module after adding the definition file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738248049/Medusa%20Resources/custom-item-price-3_imtbuw.jpg)

```ts title="src/modules/metal-prices/index.ts"
import { Module } from "@medusajs/framework/utils"
import MetalPricesModuleService from "./service"

export const METAL_PRICES_MODULE = "metal-prices"

export default Module(METAL_PRICES_MODULE, {
  service: MetalPricesModuleService,
})
```

You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `metal-prices`.
2. An object with a required property `service` indicating the module's service.

### Add Module to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/metal-prices",
      options: {
        accessToken: process.env.GOLD_API_TOKEN,
        sandbox: process.env.GOLD_API_SANDBOX === "true",
      },
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

The object also has an `options` property that accepts the module's options. You set the `accessToken` and `sandbox` options based on environment variables.

You'll find the access token at the top of your GoldAPI.io dashboard.

![The access token is below the "API Token" header of your GoldAPI.io dashboard.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738248335/Medusa%20Resources/Screenshot_2025-01-30_at_4.44.07_PM_xht3j4.png)

Set the access token as an environment variable in `.env`:

```bash
GOLD_API_TOKEN=
```

You'll start using the module in the next steps.

***

## Step 3: Add Custom Item to Cart Workflow

In this section, you'll implement the logic to retrieve the real-time price of a variant based on the metals in it, then add the variant to the cart with the custom price. You'll implement this logic in a workflow.

A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an endpoint.

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md)

The workflow you'll implement in this section has the following steps:

- [useQueryGraphStep (Retrieve Cart)](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's ID and currency using Query.
- [useQueryGraphStep (Retrieve Variant)](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the variant's details using Query
- [getVariantMetalPricesStep](#getvariantmetalpricesstep): Retrieve the variant's price using the third-party service.
- [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md): Acquire a lock on the cart to prevent concurrent modifications.
- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md): Add the item with the custom price to the cart.
- [useQueryGraphStep (Retrieve Cart)](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the updated cart's details using Query.
- [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md): Release the lock on the cart.

You'll only implement the `getVariantMetalPricesStep`. Medusa provides the other steps out-of-the-box.

### getVariantMetalPricesStep

The `getVariantMetalPricesStep` will retrieve the real-time metal price of a variant received as an input.

To create the step, create the file `src/workflows/steps/get-variant-metal-prices.ts` with the following content:

![The directory structure after adding the step file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738249036/Medusa%20Resources/custom-item-price-4_kumzdc.jpg)

```ts title="src/workflows/steps/get-variant-metal-prices.ts"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { ProductVariantDTO } from "@medusajs/framework/types"
import { METAL_PRICES_MODULE } from "../../modules/metal-prices"
import MetalPricesModuleService from "../../modules/metal-prices/service"

export type GetVariantMetalPricesStepInput = {
  variant: ProductVariantDTO & {
    calculated_price?: {
      calculated_amount: number
    }
  }
  currencyCode: string
  quantity?: number
}

export const getVariantMetalPricesStep = createStep(
  "get-variant-metal-prices",
  async ({
    variant,
    currencyCode,
    quantity = 1,
  }: GetVariantMetalPricesStepInput, { container }) => {
    const metalPricesModuleService: MetalPricesModuleService = 
      container.resolve(METAL_PRICES_MODULE)

    // TODO 
  }
)
```

You create a step with `createStep` from the Workflows SDK. It accepts two parameters:

1. The step's unique name, which is `get-variant-metal-prices`.
2. An async function that receives two parameters:
   - An input object with the variant, currency code, and quantity. The variant has a `calculated_price` property that holds the variant's fixed price in the Medusa application. This is useful when you want to add a fixed price to the real-time custom price, such as handling fees.
   - The [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.

In the step function, so far you only resolve the Metal Prices Module's service from the Medusa container.

Next, you'll validate that the specified variant can have its price calculated. Add the following import at the top of the file:

```ts title="src/workflows/steps/get-variant-metal-prices.ts"
import { MedusaError } from "@medusajs/framework/utils"
```

And replace the `TODO` in the step function with the following:

```ts title="src/workflows/steps/get-variant-metal-prices.ts"
const variantMetal = variant.options.find(
  (option) => option.option?.title === "Metal"
)?.value
const metalSymbol = await metalPricesModuleService
  .getMetalSymbol(variantMetal || "")

if (!metalSymbol) {
  throw new MedusaError(
    MedusaError.Types.INVALID_DATA,
    "Variant doesn't have metal. Make sure the variant's SKU matches a metal symbol."
  )
}

if (!variant.weight) {
  throw new MedusaError(
    MedusaError.Types.INVALID_DATA,
    "Variant doesn't have weight. Make sure the variant has weight to calculate its price."
  )
}

// TODO retrieve custom price
```

In the code above, you first retrieve the metal option's value from the variant's options, assuming that a variant has metals if it has a `Metal` option. Then, you retrieve the metal symbol of the option's value using the `getMetalSymbol` method of the Metal Prices Module's service.

If the variant doesn't have a metal in its options, the option's value is not valid, or the variant doesn't have a weight, you throw an error. The weight is necessary to calculate the price based on the metal's price per weight.

Next, you'll retrieve the real-time price of the metal using the third-party service. Replace the `TODO` with the following:

```ts title="src/workflows/steps/get-variant-metal-prices.ts"
let price = variant.calculated_price?.calculated_amount || 0
const weight = variant.weight
const { price: metalPrice } = await metalPricesModuleService.getMetalPrice(
  metalSymbol as MetalSymbols, currencyCode
)
price += (metalPrice * weight * quantity)

return new StepResponse(price)
```

In the code above, you first set the price to the variant's fixed price, if it has one. Then, you retrieve the metal's price using the `getMetalPrice` method of the Metal Prices Module's service.

Finally, you calculate the price by multiplying the metal's price by the variant's weight and the quantity to add to the cart, then add the fixed price to it.

Every step must return a `StepResponse` instance. The `StepResponse` constructor accepts the step's output as a parameter, which in this case is the variant's price.

### Create addCustomToCartWorkflow

Now that you have the `getVariantMetalPricesStep`, you can create the workflow that adds the item with custom pricing to the cart.

Create the file `src/workflows/add-custom-to-cart.ts` with the following content:

![The directory structure after adding the workflow file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738251380/Medusa%20Resources/custom-item-price-5_zorahv.jpg)

```ts title="src/workflows/add-custom-to-cart.ts" highlights={workflowHighlights}
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { QueryContext } from "@medusajs/framework/utils"

type AddCustomToCartWorkflowInput = {
  cart_id: string
  item: {
    variant_id: string
    quantity: number
    metadata?: Record<string, unknown>
  }
}

export const addCustomToCartWorkflow = createWorkflow(
  "add-custom-to-cart",
  ({ cart_id, item }: AddCustomToCartWorkflowInput) => {
    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      filters: { id: cart_id },
      fields: ["id", "currency_code"],
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const { data: variants } = useQueryGraphStep({
      entity: "variant",
      fields: [
        "*",
        "options.*",
        "options.option.*",
        "calculated_price.*",
      ],
      filters: {
        id: item.variant_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
      context: {
        calculated_price: QueryContext({
          currency_code: carts[0].currency_code,
        }),
      },
    }).config({ name: "retrieve-variant" })

    // TODO add more steps
  }
)
```

You create a workflow with `createWorkflow` from the Workflows SDK. It accepts two parameters:

1. The workflow's unique name, which is `add-custom-to-cart`.
2. A function that receives an input object with the cart's ID and the item to add to the cart. The item has the variant's ID, quantity, and optional metadata.

In the function, you first retrieve the cart's details using the `useQueryGraphStep` helper step. This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) which is a Modules SDK tool that retrieves data across modules. You use it to retrieve the cart's ID and currency code.

You also retrieve the variant's details using the `useQueryGraphStep` helper step. You pass the variant's ID to the step's filters and specify the fields to retrieve. To retrieve the variant's price based on the cart's context, you pass the cart's currency code to the `calculated_price` context.

Next, you'll retrieve the variant's real-time price using the `getVariantMetalPricesStep` you created earlier. First, add the following import:

```ts title="src/workflows/add-custom-to-cart.ts"
import { 
  getVariantMetalPricesStep, 
  GetVariantMetalPricesStepInput,
} from "./steps/get-variant-metal-prices"
```

Then, replace the `TODO` in the workflow with the following:

```ts title="src/workflows/add-custom-to-cart.ts"
const price = getVariantMetalPricesStep({
  variant: variants[0],
  currencyCode: carts[0].currency_code,
  quantity: item.quantity,
} as unknown as GetVariantMetalPricesStepInput)

// TODO add item with custom price to cart
```

You execute the `getVariantMetalPricesStep` passing it the variant's details, the cart's currency code, and the quantity of the item to add to the cart. The step returns the variant's custom price.

Next, you'll add the item with the custom price to the cart. First, add the following imports at the top of the file:

```ts title="src/workflows/add-custom-to-cart.ts"
import { transform } from "@medusajs/framework/workflows-sdk"
import { 
  acquireLockStep, 
  addToCartWorkflow, 
} from "@medusajs/medusa/core-flows"
```

Then, replace the `TODO` in the workflow with the following:

```ts title="src/workflows/add-custom-to-cart.ts"
const itemToAdd = transform({
  item,
  price,
}, (data) => {
  return [{
    ...data.item,
    unit_price: data.price,
  }]
})

acquireLockStep({
  key: cart_id,
  timeout: 2,
  ttl: 10,
})

addToCartWorkflow.runAsStep({
  input: {
    items: itemToAdd,
    cart_id,
  },
})

// TODO retrieve and return cart
```

You prepare the item to add to the cart using `transform` from the Workflows SDK. It allows you to manipulate and create variables in a workflow.

After that, you use Medusa's `acquireLockStep` to acquire a lock on the cart, and `addToCartWorkflow` to add the item with the custom price to the cart.

A workflow's constructor function has some constraints in implementation, which is why you need to use `transform` for variable manipulation. Learn more about these constraints in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md).

Lastly, you'll retrieve the cart's details again and return them. Add the following import at the beginning of the file:

```ts title="src/workflows/add-custom-to-cart.ts"
import { WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { 
  releaseLockStep,
} from "@medusajs/medusa/core-flows"
```

And replace the last `TODO` in the workflow with the following:

```ts title="src/workflows/add-custom-to-cart.ts"
const { data: updatedCarts } = useQueryGraphStep({
  entity: "cart",
  filters: { id: cart_id },
  fields: ["id", "items.*"],
}).config({ name: "refetch-cart" })

releaseLockStep({
  key: cart_id,
})

return new WorkflowResponse({
  cart: updatedCarts[0],
})
```

In the code above, you retrieve the updated cart's details using the `useQueryGraphStep` helper step. Then, you release the lock on the cart with the `releaseLockStep`.

To return data from the workflow, you create and return a `WorkflowResponse` instance. It accepts as a parameter the data to return, which is the updated cart.

In the next step, you'll use the workflow in a custom route to add an item with a custom price to the cart.

***

## Step 4: Create Add Custom Item to Cart API Route

Now that you've implemented the logic to add an item with a custom price to the cart, you'll expose this functionality in an API route.

An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts. You'll create an API route at the path `/store/carts/:id/line-items-metals` that executes the workflow from the previous step to add a product variant with custom price to the cart.

Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

### Create API Route

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory.

The path of the API route is the file's path relative to `src/api`. So, to create the `/store/carts/:id/line-items-metals` API route, create the file `src/api/store/carts/[id]/line-items-metals/route.ts` with the following content:

![The directory structure after adding the API route file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738252712/Medusa%20Resources/custom-item-price-6_deecbu.jpg)

```ts title="src/api/store/carts/[id]/line-items-metals/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
import { HttpTypes } from "@medusajs/framework/types"
import { addCustomToCartWorkflow } from "../../../../../workflows/add-custom-to-cart"

export const POST = async (
  req: MedusaRequest<HttpTypes.StoreAddCartLineItem>, 
  res: MedusaResponse
) => {
  const { id } = req.params
  const item = req.validatedBody

  const { result } = await addCustomToCartWorkflow(req.scope)
    .run({
      input: {
        cart_id: id,
        item,
      },
    })

  res.status(200).json({ cart: result.cart })
}
```

Since you export a `POST` function in this file, you're exposing a `POST` API route at `/store/carts/:id/line-items-metals`. The route handler function accepts two parameters:

1. A request object with details and context on the request, such as path and body parameters.
2. A response object to manipulate and send the response.

In the function, you retrieve the cart's ID from the path parameter, and the item's details from the request body. This API route will accept the same request body parameters as Medusa's [Add Item to Cart API Route](https://docs.medusajs.com/api/store#carts_postcartsidlineitems).

Then, you execute the `addCustomToCartWorkflow` by invoking it, passing it the Medusa container, which is available in the request's `scope` property, then executing its `run` method. You pass the workflow's input object with the cart's ID and the item to add to the cart.

Finally, you return a response with the updated cart's details.

### Add Request Body Validation Middleware

To ensure that the request body contains the required parameters, you'll add a middleware that validates the incoming request's body based on a defined schema.

A middleware is a function executed before the API route when a request is sent to it. You define middlewares in Medusa in the `src/api/middlewares.ts` directory.

Learn more about middlewares in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

To add a validation middleware to the custom API route, create the file `src/api/middlewares.ts` with the following content:

![The directory structure after adding the middleware file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738253099/Medusa%20Resources/custom-item-price-7_l7iw2a.jpg)

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { 
  StoreAddCartLineItem,
} from "@medusajs/medusa/api/store/carts/validators"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/store/carts/:id/line-items-metals",
      method: "POST",
      middlewares: [
        validateAndTransformBody(
          StoreAddCartLineItem
        ),
      ],
    },
  ],
})
```

In this file, you export the middlewares definition using `defineMiddlewares` from the Medusa Framework. This function accepts an object having a `routes` property, which is an array of middleware configurations to apply on routes.

You pass in the `routes` array an object having the following properties:

- `matcher`: The route to apply the middleware on.
- `method`: The HTTP method to apply the middleware on for the specified API route.
- `middlewares`: An array of the middlewares to apply. You apply the `validateAndTransformBody` middleware, which validates the request body based on the `StoreAddCartLineItem` schema. This validation schema is the same schema used for Medusa's [Add Item to Cart API Route](https://docs.medusajs.com/api/store#carts_postcartsidlineitems).

Any request sent to the `/store/carts/:id/line-items-metals` API route will now fail if it doesn't have the required parameters.

Learn more about API route validation in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/validation/index.html.md).

### Prepare to Test API Route

Before you test the API route, you'll prepare and retrieve the necessary data to add a product variant with a custom price to the cart.

#### Create Product with Metal Variant

You'll first create a product that has a `Metal` option, and variant(s) with values for this option.

Start the Medusa application with the following command:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard at `localhost:9000/app` and log in with the email and password you created when you installed the Medusa application in the first step.

Once you log in, click on Products in the sidebar, then click the Create button at the top right.

![Click on Products in the sidebar at the left, then click on the Create button at the top right of the content](https://res.cloudinary.com/dza7lstvk/image/upload/v1738253415/Medusa%20Resources/Screenshot_2025-01-30_at_6.09.36_PM_ee0jr2.png)

Then, in the Create Product form:

1. Enter a name for the product, and optionally enter other details like description.
2. Enable the "Yes, this is a product with variants" toggle.
3. Under Product Options, enter "Metal" for the title, and enter "Gold" for the values.

Once you're done, click the Continue button.

![Fill in the product details, enable the "Yes, this is a product with variants" toggle, and add the "Metal" option with "Gold" value](https://res.cloudinary.com/dza7lstvk/image/upload/v1738253520/Medusa%20Resources/Screenshot_2025-01-30_at_6.11.29_PM_lqxth9.png)

You can skip the next two steps by clicking the Continue button again, then the Publish button.

Once you're done, the product's page will open. You'll now add weight to the product's Gold variant. To do that:

- Scroll to the Variants section and find the Gold variant.
- Click on the three-dots icon at its right.
- Choose "Edit" from the dropdown.

![Find the Gold variant in the Variants section, click on the three-dots icon, and choose "Edit"](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254038/Medusa%20Resources/Screenshot_2025-01-30_at_6.19.52_PM_j3hjcx.png)

In the side window that opens, find the Weight field, enter the weight, and click the Save button.

![Enter the weight in the Weight field, then click the Save button](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254165/Medusa%20Resources/Screenshot_2025-01-30_at_6.22.15_PM_yplzdp.png)

Finally, you need to set fixed prices for the variant, even if they're just `0`. To do that:

1. Click on the three-dots icon at the top right of the Variants section.
2. Choose "Edit Prices" from the dropdown.

![Click on the three-dots icon at the top right of the Variants section, then choose "Edit Prices"](https://res.cloudinary.com/dza7lstvk/image/upload/v1738255203/Medusa%20Resources/Screenshot_2025-01-30_at_6.39.35_PM_s3jpxh.png)

For each cell in the table, either enter a fixed price for the specified currency or leave it as `0`. Once you're done, click the Save button.

![Enter fixed prices for the variant in the table, then click the Save button](https://res.cloudinary.com/dza7lstvk/image/upload/v1738255272/Medusa%20Resources/Screenshot_2025-01-30_at_6.40.45_PM_zw1l59.png)

You'll use this variant to add it to the cart later. You can find its ID by clicking on the variant, opening its details page. Then, on the details page, click on the icon at the right of the JSON section, and copy the ID from the JSON data.

![Click on the icon at the right of the JSON section to copy the variant's ID](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254314/Medusa%20Resources/Screenshot_2025-01-30_at_6.24.49_PM_ka7xew.png)

#### Retrieve Publishable API Key

All requests sent to API routes starting with `/store` must have a publishable API key in the header. This ensures the request's operations are scoped to the publishable API key's associated sales channels. For example, products that aren't available in a cart's sales channel can't be added to it.

To retrieve the publishable API key, on the Medusa Admin:

1. Click on Settings in the sidebar at the bottom left.
2. Click on Publishable API Keys from the sidebar, then click on a publishable API key in the list.

![Click on publishable API keys in the Settings sidebar, then click on a publishable API key in the list](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254523/Medusa%20Resources/Screenshot_2025-01-30_at_6.28.17_PM_mldscc.png)

3. Click on the publishable API key to copy it.

![Click on the publishable API key to copy it](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254601/Medusa%20Resources/Screenshot_2025-01-30_at_6.29.26_PM_vvatki.png)

You'll use this key when you test the API route.

### Test API Route

To test out the API route, you need to create a cart. A cart must be associated with a region. So, to retrieve the ID of a region in your store, send a `GET` request to the `/store/regions` API route:

```bash
curl 'localhost:9000/store/regions' \
-H 'x-publishable-api-key: {api_key}'
```

Make sure to replace `{api_key}` with the publishable API key you copied earlier.

This will return a list of regions. Copy the ID of one of the regions.

Then, send a `POST` request to the `/store/carts` API route to create a cart:

```bash
curl -X POST 'localhost:9000/store/carts' \
-H 'x-publishable-api-key: {api_key}' \
-H 'Content-Type: application/json' \
--data '{
    "region_id": "{region_id}"
}'
```

Make sure to replace `{api_key}` with the publishable API key you copied earlier, and `{region_id}` with the ID of a region from the previous request.

This will return the created cart. Copy the ID of the cart to use it next.

Finally, to add the Gold variant to the cart with a custom price, send a `POST` request to the `/store/carts/:id/line-items-metals` API route:

```bash
curl -X POST 'localhost:9000/store/carts/{cart_id}/line-items-metals' \
-H 'x-publishable-api-key: {api_key}' \
-H 'Content-Type: application/json' \
--data '{
    "variant_id": "{variant_id}",
    "quantity": 1
}'
```

Make sure to replace:

- `{api_key}` with the publishable API key you copied earlier.
- `{cart_id}` with the ID of the cart you created.
- `{variant_id}` with the ID of the Gold variant you created.

This will return the cart's details, where you can see in its `items` array the item with the custom price:

```json title="Example Response"
{
  "cart": {
    "items": [
      {
        "variant_id": "{variant_id}",
        "quantity": 1,
        "is_custom_price": true,
        // example custom price
        "unit_price": 2000
      }
    ]
  }
}
```

The price will be the result of the calculation you've implemented earlier, which is the fixed price of the variant plus the real-time price of the metal, multiplied by the weight of the variant and the quantity added to the cart.

This price will be reflected in the cart's total price, and you can proceed to checkout with the custom-priced item.

***

## Next Steps

You've now implemented custom item pricing in Medusa. You can also customize the [storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to use the new API route to add custom-priced items to the cart.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Implement Quote Management in Medusa

In this guide, you'll learn how to implement quote management in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) which are available out-of-the-box.

By default, the Medusa application provides standard commerce features for orders and carts. However, Medusa's customization capabilities facilitate extending existing features to implement quote-management features.

By building quote management features, you allow customers to request a quote for a set of products and, once the merchant and customer reach an agreement, you create an order for that quote. Quote management is useful in many use cases, including B2B stores.

This guide is based on the [B2B starter](https://github.com/medusajs/b2b-starter-medusa) explaining how to implement some of its quote management features. You can refer to the B2B starter for other features not covered in this guide.

## Summary

By following this guide, you'll add the following features to Medusa:

1. Customers can request a quote for a set of products.
2. Merchants can manage quotes in the Medusa Admin dashboard. They can reject a quote or send a counter-offer, and they can make edits to item prices and quantities.
3. Customers can accept or reject a quote once it's been sent by the merchant.
4. Once the customer accepts a quote, it's converted to an order in Medusa.

![Quote management system workflow diagram illustrating the complete quote lifecycle: customers request quotes from cart items, merchants review and create detailed quotes with pricing, quotes are sent to customers for approval, and upon acceptance, quotes are automatically converted into processable orders within the Medusa e-commerce platform](https://res.cloudinary.com/dza7lstvk/image/upload/v1741173690/Medusa%20Resources/quote-management-summary_xd319j.jpg)

To implement these features, you'll be customizing the Medusa server and the Medusa Admin dashboard.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer.

- [Quote Management Repository](https://github.com/medusajs/examples/tree/main/quote-management): Find the full code for this guide in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1741171875/OpenApi/quote-management_tbk552.yml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. You can also optionally choose to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Add Quote Module

In Medusa, you can build custom features in a [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

In the module, you define the data models necessary for a feature and the logic to manage these data models. Later, you can build commerce flows around your module and link its data models to other modules' data models, such as orders and carts.

In this step, you'll build a Quote Module that defines the necessary data model to store quotes.

Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).

### Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/quote`.

![Diagram showcasing the directory structure after adding the Quote Module's directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1741074268/Medusa%20Resources/quote-1_lxgyyg.jpg)

### Create Data Models

A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

Learn more about data models in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md).

For the Quote Module, you need to define a `Quote` data model that represents a quote requested by a customer.

So, start by creating the `Quote` data model. Create the file `src/modules/quote/models/quote.ts` with the following content:

![Diagram showcasing the directory structure after adding the quote model](https://res.cloudinary.com/dza7lstvk/image/upload/v1741074453/Medusa%20Resources/quote-2_lh012l.jpg)

```ts title="src/modules/quote/models/quote.ts" highlights={quoteModelHighlights}
import { model } from "@medusajs/framework/utils"

export enum QuoteStatus {
  PENDING_MERCHANT = "pending_merchant",
  PENDING_CUSTOMER = "pending_customer",
  ACCEPTED = "accepted",
  CUSTOMER_REJECTED = "customer_rejected",
  MERCHANT_REJECTED = "merchant_rejected",
}

export const Quote = model.define("quote", {
  id: model.id().primaryKey(),
  status: model
    .enum(Object.values(QuoteStatus))
    .default(QuoteStatus.PENDING_MERCHANT),
  customer_id: model.text(),
  draft_order_id: model.text(),
  order_change_id: model.text(),
  cart_id: model.text(),
})
```

You define the `Quote` data model using the `model.define` method of the DML. It accepts the data model's table name as a first parameter, and the model's schema object as a second parameter.

`Quote` has the following properties:

- `id`: A unique identifier for the quote.
- `status`: The status of the quote, which can be one of the following:
  - `pending_merchant`: The quote is pending the merchant's approval or rejection.
  - `pending_customer`: The quote is pending the customer's acceptance or rejection.
  - `accepted`: The quote has been accepted by the customer and converted to an order.
  - `customer_rejected`: The customer has rejected the quote.
  - `merchant_rejected`: The merchant has rejected the quote.
- `customer_id`: The ID of the customer who requested the quote. You'll later learn how to link this to a customer record.
- `draft_order_id`: The ID of the draft order created for the quote. You'll later learn how to link this to an order record.
- `order_change_id`: The ID of the order change created for the quote. An [order change](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) is a record of changes made to an order, such as price or quantity updates of the order's items. These changes are later applied to the order. You'll later learn how to link this to an order change record.
- `cart_id`: The ID of the cart that the quote was created from. The cart will hold the items that the customer wants a quote for. You'll later learn how to link this to a cart record.

Learn more about defining data model properties in the [Property Types documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md).

### Create Module's Service

You now have the necessary data model in the Quote Module, but you need to define the logic to manage it. You do this by creating a service in the module.

A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, allowing you to manage your data models, or connect to a third-party service, which is useful if you're integrating with external services.

Learn more about services in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md).

To create the Quote Module's service, create the file `src/modules/quote/service.ts` with the following content:

![Directory structure after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1741075946/Medusa%20Resources/quote-4_hg4bnr.jpg)

```ts title="src/modules/quote/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import { Quote } from "./models/quote"

class QuoteModuleService extends MedusaService({ 
  Quote, 
}) {}

export default QuoteModuleService
```

The `QuoteModuleService` extends `MedusaService` from the Modules SDK which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods.

So, the `QuoteModuleService` class now has methods like `createQuotes` and `retrieveQuote`.

Find all methods generated by the `MedusaService` in [this reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md).

You'll use this service later when you implement custom flows for quote management.

### Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/quote/index.ts` with the following content:

![Directory structure after adding the module definition](https://res.cloudinary.com/dza7lstvk/image/upload/v1741076106/Medusa%20Resources/quote-5_ngitn1.jpg)

```ts title="src/modules/quote/index.ts"
import { Module } from "@medusajs/framework/utils"
import QuoteModuleService from "./service"

export const QUOTE_MODULE = "quote"

export default Module(QUOTE_MODULE, { 
  service: QuoteModuleService,
})
```

You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `quote`.
2. An object with a required property `service` indicating the module's service.

You also export the module's name as `QUOTE_MODULE` so you can reference it later.

### Add Module to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/quote",
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

### Generate Migrations

Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module.

Learn more about migrations in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md).

Medusa's CLI tool generates the migrations for you. To generate a migration for the Quote Module, run the following command in your Medusa application's directory:

```bash
npx medusa db:generate quote
```

The `db:generate` command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a `migrations` directory under `src/modules/quote` that holds the generated migration.

![The directory structure of the Quote Module after generating the migration](https://res.cloudinary.com/dza7lstvk/image/upload/v1741076301/Medusa%20Resources/quote-6_adzf76.jpg)

Then, to reflect these migrations on the database, run the following command:

```bash
npx medusa db:migrate
```

The table for the `Quote` data model is now created in the database.

***

## Step 3: Define Links to Other Modules

When you defined the `Quote` data model, you added properties that store the ID of records managed by other modules. For example, the `customer_id` property stores the ID of the customer that requested the quote, but customers are managed by the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md).

Medusa integrates modules into your application without implications or side effects by isolating modules from one another. This means you can't directly create relationships between data models in your module and data models in other modules.

Instead, Medusa provides the mechanism to define links between data models, and retrieve and manage linked records while maintaining module isolation. Links are useful to define associations between data models in different modules, or extend a model in another module to associate custom properties with it.

To learn more about module isolation, refer to the [Module Isolation documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

In this step, you'll define the following links between the Quote Module's data model and data models in other modules:

1. `Quote` \<> `Cart` data model of the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md): link quotes to the carts they were created from.
2. `Quote` \<> `Customer` data model of the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md): link quotes to the customers who requested them.
3. `Quote` \<> `OrderChange` data model of the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md): link quotes to the order changes that record adjustments made to the quote's draft order.
4. `Quote` \<> `Order` data model of the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md): link quotes to their draft orders that are later converted to orders.

### Define Quote \<> Cart Link

You can define links between data models in a TypeScript or JavaScript file under the `src/links` directory. So, to define the link between the `Quote` and `Cart` data models, create the file `src/links/quote-cart.ts` with the following content:

![Directory structure after adding the quote-cart link](https://res.cloudinary.com/dza7lstvk/image/upload/v1741077395/Medusa%20Resources/quote-7_xrvodi.jpg)

```ts title="src/links/quote-cart.ts" highlights={quoteCartHighlights}
import { defineLink } from "@medusajs/framework/utils"
import QuoteModule from "../modules/quote"
import CartModule from "@medusajs/medusa/cart"

export default defineLink(
  {
    linkable: QuoteModule.linkable.quote.id,
    field: "cart_id",
  },
  CartModule.linkable.cart,
  {
    readOnly: true,
  }
)
```

You define a link using the `defineLink` function from the Modules SDK. It accepts three parameters:

1. An object indicating the first data model part of the link. A module has a special `linkable` property that contains link configurations for its data models. So, you can pass the link configurations for the `Quote` data model from the `QuoteModule` module, specifying that its `cart_id` property holds the ID of the linked record.
2. An object indicating the second data model part of the link. You pass the link configurations for the `Cart` data model from the `CartModule` module.
3. An optional object with additional configurations for the link. By default, Medusa creates a table in the database to represent the link you define. However, when you only want to retrieve the linked records without managing and storing the links, you can set the `readOnly` option to `true`.

You'll now be able to retrieve the cart that a quote was created from, as you'll see in later steps.

### Define Quote \<> Customer Link

Next, you'll define the link between the `Quote` and `Customer` data model of the Customer Module. So, create the file `src/links/quote-customer.ts` with the following content:

![Directory structure after adding the quote-customer link](https://res.cloudinary.com/dza7lstvk/image/upload/v1741078047/Medusa%20Resources/quote-8_bbngmh.jpg)

```ts title="src/links/quote-customer.ts"
import { defineLink } from "@medusajs/framework/utils"
import QuoteModule from "../modules/quote"
import CustomerModule from "@medusajs/medusa/customer"

export default defineLink(
  {
    linkable: QuoteModule.linkable.quote.id,
    field: "customer_id",
  },
  CustomerModule.linkable.customer,
  {
    readOnly: true,
  }
)
```

You define the link between the `Quote` and `Customer` data models in the same way as the `Quote` and `Cart` link. In the first object parameter of `defineLink`, you pass the linkable configurations of the `Quote` data model, specifying the `customer_id` property as the link field. In the second object parameter, you pass the linkable configurations of the `Customer` data model from the Customer Module. You also configure the link to be read-only.

### Define Quote \<> OrderChange Link

Next, you'll define the link between the `Quote` and `OrderChange` data model of the Order Module. So, create the file `src/links/quote-order-change.ts` with the following content:

![Directory structure after adding the quote-order-change link](https://res.cloudinary.com/dza7lstvk/image/upload/v1741078511/Medusa%20Resources/quote-11_faac5m.jpg)

```ts title="src/links/quote-order-change.ts"
import { defineLink } from "@medusajs/framework/utils"
import QuoteModule from "../modules/quote"
import OrderModule from "@medusajs/medusa/order"

export default defineLink(
  {
    linkable: QuoteModule.linkable.quote.id,
    field: "order_change_id",
  },
  OrderModule.linkable.orderChange,
  {
    readOnly: true,
  }
)
```

You define the link between the `Quote` and `OrderChange` data models in the same way as the previous links. You pass the linkable configurations of the `Quote` data model, specifying the `order_change_id` property as the link field. In the second object parameter, you pass the linkable configurations of the `OrderChange` data model from the Order Module. You also configure the link to be read-only.

### Define Quote \<> Order Link

Finally, you'll define the link between the `Quote` and `Order` data model of the Order Module. So, create the file `src/links/quote-order.ts` with the following content:

![Directory structure after adding the quote-order link](https://res.cloudinary.com/dza7lstvk/image/upload/v1741078607/Medusa%20Resources/quote-12_ixr2f7.jpg)

```ts title="src/links/quote-order.ts"
import { defineLink } from "@medusajs/framework/utils"
import QuoteModule from "../modules/quote"
import OrderModule from "@medusajs/medusa/order"

export default defineLink(
  {
    linkable: QuoteModule.linkable.quote.id,
    field: "draft_order_id",
  },
  {
    linkable: OrderModule.linkable.order.id,
    alias: "draft_order",
  },
  {
    readOnly: true,
  }
)
```

You define the link between the `Quote` and `Order` data models similar to the previous links. You pass the linkable configurations of the `Quote` data model, specifying the `draft_order_id` property as the link field.

In the second object parameter, you pass the linkable configurations of the `Order` data model from the Order Module. You also set an `alias` property to `draft_order`. This allows you later to retrieve the draft order of a quote with the `draft_order` alias rather than the default `order` alias. Finally, you configure the link to be read-only.

You've finished creating the links that allow you to retrieve data related to quotes. You'll see how to use these links in later steps.

***

## Step 4: Implement Create Quote Workflow

You're now ready to start implementing quote-management features. The first one you'll implement is the ability for customers to request a quote for a set of items in their cart.

To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an endpoint.

So, in this section, you'll learn how to create a workflow that creates a quote for a customer.

Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

The workflow will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart that the customer wants a quote for.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the customer requesting the quote.
- [createOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderWorkflow/index.html.md): Create the draft order for the quote.
- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md): Create the order change for the draft order.
- [createQuotesStep](#createQuotesStep): Create the quote for the customer.

The first four steps are provided by Medusa in its `@medusajs/medusa/core-flows` package. So, you only need to implement the `createQuotesStep` step.

### createQuotesStep

In the last step of the workflow, you'll create a quote for the customer using the Quote Module's service.

To create a step, create the file `src/workflows/steps/create-quotes.ts` with the following content:

![Directory structure after adding the create-quotes step](https://res.cloudinary.com/dza7lstvk/image/upload/v1741085446/Medusa%20Resources/quote-13_tv9i23.jpg)

```ts title="src/workflows/steps/create-quotes.ts" highlights={createQuotesStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { QUOTE_MODULE } from "../../modules/quote"
import QueryModuleService from "../../modules/quote/service"

type StepInput = {
  draft_order_id: string;
  order_change_id: string;
  cart_id: string;
  customer_id: string;
}[]

export const createQuotesStep = createStep(
  "create-quotes",
  async (input: StepInput, { container }) => {
    const quoteModuleService: QueryModuleService = container.resolve(
      QUOTE_MODULE
    )

    const quotes = await quoteModuleService.createQuotes(input)

    return new StepResponse(
      quotes,
      quotes.map((quote) => quote.id)
    )
  }
)
```

You create a step with `createStep` from the Workflows SDK. It accepts two parameters:

1. The step's unique name, which is `create-quotes`.
2. An async function that receives two parameters:
   - The step's input, which is in this case an array of quotes to create.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.

In the step function, you resolve the Quote Module's service from the Medusa container using the `resolve` method of the container, passing it the module's name as a parameter.

Then, you create the quotes using the `createQuotes` method. As you remember, the Quote Module's service extends the `MedusaService` which generates data-management methods for you.

A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters:

1. The step's output, which is the quotes created.
2. Data to pass to the step's compensation function, which you'll add next.

#### Add Compensation to Step

A step can have a compensation function that undoes the actions performed in a step. Then, if an error occurs during the workflow's execution, the compensation functions of executed steps are called to roll back the changes. This mechanism ensures data consistency in your application, especially as you integrate external systems.

To add a compensation function to a step, pass it as a third-parameter to `createStep`:

```ts title="src/workflows/steps/create-quotes.ts"
export const createQuotesStep = createStep(
  // ...
  async (quoteIds, { container }) => {
    if (!quoteIds) {
      return
    }
    
    const quoteModuleService: QueryModuleService = container.resolve(
      QUOTE_MODULE
    )

    await quoteModuleService.deleteQuotes(quoteIds)
  }
)
```

The compensation function accepts two parameters:

1. The data passed from the step in the second parameter of `StepResponse`, which in this case is an array of quote IDs.
2. An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

In the compensation function, you resolve the Quote Module's service from the Medusa container and call the `deleteQuotes` method to delete the quotes created in the step.

### createRequestForQuoteWorkflow

You can now create the workflow using the steps provided by Medusa and your custom step.

To create the workflow, create the file `src/workflows/create-request-for-quote.ts` with the following content:

```ts title="src/workflows/create-request-for-quote.ts" highlights={createRequestForQuoteHighlights} collapsibleLines="1-20" expandButtonLabel="Show Imports"
import {
  beginOrderEditOrderWorkflow,
  createOrderWorkflow,
  CreateOrderWorkflowInput,
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { OrderStatus } from "@medusajs/framework/utils"
import {
  createWorkflow,
  transform,
  WorkflowResponse,
} from "@medusajs/workflows-sdk"
import { CreateOrderLineItemDTO } from "@medusajs/framework/types"
import { createQuotesStep } from "./steps/create-quotes"

type WorkflowInput = {
  cart_id: string;
  customer_id: string;
};

export const createRequestForQuoteWorkflow = createWorkflow(
  "create-request-for-quote",
  (input: WorkflowInput) => {
    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      fields: [
        "id",
        "sales_channel_id",
        "currency_code",
        "region_id",
        "customer.id",
        "customer.email",
        "shipping_address.*",
        "billing_address.*",
        "items.*",
        "shipping_methods.*",
        "promotions.code",
      ],
      filters: { id: input.cart_id },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const { data: customers } = useQueryGraphStep({
      entity: "customer",
      fields: ["id", "customer"],
      filters: { id: input.customer_id },
      options: {
        throwIfKeyNotFound: true,
      },
    }).config({ name: "customer-query" })

    // TODO create order
  }
)
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is an object having the ID of the customer requesting the quote, and the ID of their cart.

In the workflow's constructor function, you use `useQueryGraphStep` to retrieve the cart and customer details using the IDs passed as an input to the workflow.

`useQueryGraphStep` uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which allows you to retrieve data across modules. For example, in the above snippet you're retrieving the cart's promotions, which are managed in the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md), by passing `promotions.code` to the `fields` array.

Next, you want to create the draft order for the quote. Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/create-request-for-quote.ts"
const orderInput = transform({ carts, customers }, ({ carts, customers }) => {
  return {
    is_draft_order: true,
    status: OrderStatus.DRAFT,
    sales_channel_id: carts[0].sales_channel_id || undefined,
    email: customers[0].email || undefined,
    customer_id: customers[0].id || undefined,
    billing_address: carts[0].billing_address,
    shipping_address: carts[0].shipping_address,
    items: carts[0].items as CreateOrderLineItemDTO[] || [],
    region_id: carts[0].region_id || undefined,
    promo_codes: carts[0].promotions?.map((promo) => promo?.code),
    currency_code: carts[0].currency_code,
    shipping_methods: carts[0].shipping_methods || [],
  } as CreateOrderWorkflowInput
})

const draftOrder = createOrderWorkflow.runAsStep({
  input: orderInput,
})

// TODO create order change
```

You first prepare the order's details using `transform` from the Workflows SDK. Since Medusa creates an internal representation of the workflow's constructor before any data actually has a value, you can't manipulate data directly in the function. So, Medusa provides utilities like `transform` to manipulate data instead. You can learn more in the [transform variables](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) documentation.

Then, you create the draft order using the `createOrderWorkflow` workflow which you imported from `@medusajs/medusa/core-flows`. The workflow creates and returns the created order.

After that, you want to create an order change for the draft order. This will allow the admin later to make edits to the draft order, such as updating the prices or quantities of the items in the order.

Replace the `TODO` with the following:

```ts title="src/workflows/create-request-for-quote.ts"
const orderEditInput = transform({ draftOrder }, ({ draftOrder }) => {
  return {
    order_id: draftOrder.id,
    description: "",
    internal_note: "",
    metadata: {},
  }
})

const changeOrder = beginOrderEditOrderWorkflow.runAsStep({
  input: orderEditInput,
})

// TODO create quote
```

You prepare the order change's details using `transform` and then create the order change using the `beginOrderEditOrderWorkflow` workflow which is provided by Medusa.

Finally, you want to create the quote for the customer and return it. Replace the last `TODO` with the following:

```ts title="src/workflows/create-request-for-quote.ts"
const quoteData = transform({
  draftOrder,
  carts,
  customers,
  changeOrder,
}, ({ draftOrder, carts, customers, changeOrder }) => {
  return {
    draft_order_id: draftOrder.id,
    cart_id: carts[0].id,
    customer_id: customers[0].id,
    order_change_id: changeOrder.id,
  }
})

const quotes = createQuotesStep([
  quoteData,
])

return new WorkflowResponse({ quote: quotes[0] })
```

Similar to before, you prepare the quote's details using `transform`. Then, you create the quote using the `createQuotesStep` you implemented earlier.

A workflow must return an instance of `WorkflowResponse`. The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is an object holding the created quote in this case.

In the next step, you'll learn how to execute the workflow when a customer requests a quote.

***

## Step 5: Create Quote API Route

Now that you have the logic to create a quote for a customer, you need to expose it so that frontend clients, such as a storefront, can use it. You do this by creating an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts. You'll create an API route at the path `/store/customers/me/quotes` that executes the workflow from the previous step.

Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

### Implement API Route

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

By default, all routes starting with `/store/customers/me` require the customer to be authenticated. So, you'll be creating the API route at `/store/customers/me/quotes`.

To create the API route, create the file `src/api/store/customers/me/quotes/route.ts` with the following content:

![Directory structure after adding the store/quotes route](https://res.cloudinary.com/dza7lstvk/image/upload/v1741086995/Medusa%20Resources/quote-14_meo0yo.jpg)

```ts title="src/api/store/customers/me/quotes/route.ts" highlights={createQuoteApiHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
import {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { 
  createRequestForQuoteWorkflow,
} from "../../../../../workflows/create-request-for-quote"

type CreateQuoteType = {
  cart_id: string;
}

export const POST = async (
  req: AuthenticatedMedusaRequest<CreateQuoteType>,
  res: MedusaResponse
) => {
  const {
    result: { quote: createdQuote },
  } = await createRequestForQuoteWorkflow(req.scope).run({
    input: {
      ...req.validatedBody,
      customer_id: req.auth_context.actor_id,
    },
  })

  const query = req.scope.resolve(
    ContainerRegistrationKeys.QUERY
  )

  const {
    data: [quote],
  } = await query.graph(
    {
      entity: "quote",
      fields: req.queryConfig.fields,
      filters: { id: createdQuote.id },
    },
    { throwIfKeyNotFound: true }
  )

  return res.json({ quote })
}
```

Since you export a `POST` function in this file, you're exposing a `POST` API route at `/store/customers/me/quotes`. The route handler function accepts two parameters:

1. A request object with details and context on the request, such as body parameters or authenticated customer details.
2. A response object to manipulate and send the response.

`AuthenticatedMedusaRequest` accepts the request body's type as a type argument.

In the route handler function, you create the quote using the [createRequestForQuoteWorkflow](#createrequestforquoteworkflow) from the previous step. Then, you resolve Query from the Medusa container, which is available in the request object's `req.scope` property.

You use Query to retrieve the Quote with its fields and linked records, which you'll learn how to specify soon. Finally, you send the quote as a response.

### Add Validation Schema

The API route accepts the cart ID as a request body parameter. So, it's important to validate the body of a request before executing the route's handler. You can do this by specifying a validation schema in a middleware for the API route.

In Medusa, you create validation schemas using [Zod](https://zod.dev/) in a TypeScript file under the `src/api` directory. So, create the file `src/api/store/validators.ts` with the following content:

![Directory structure after adding the validators file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741089363/Medusa%20Resources/quote-15_iy6jem.jpg)

```ts title="src/api/store/validators.ts"
import { z } from "zod"

export type CreateQuoteType = z.infer<typeof CreateQuote>;
export const CreateQuote = z
  .object({
    cart_id: z.string().min(1),
  })
  .strict()
```

You define a `CreateQuote` schema using Zod that specifies the `cart_id` parameter as a required string.

You also export a type inferred from the schema. So, go back to `src/api/store/customers/me/quotes/route.ts` and replace the implementation of `CreateQuoteType` to import the type from the `validators.ts` file instead:

```ts title="src/api/store/customers/me/quotes/route.ts"
// other imports...
// add the following import
import { CreateQuoteType } from "../../../validators"

// remove CreateQuoteType definition

export const POST = async (
  // keep type argument the same
  req: AuthenticatedMedusaRequest<CreateQuoteType>,
  res: MedusaResponse
) => {
  // ...
}
```

### Apply Validation Schema Middleware

Now that you have the validation schema, you need to add the middleware that ensures the request body is validated before the route handler is executed. A middleware is a function executed when a request is sent to an API Route. It's executed before the route handler.

Learn more about middleware in the [Middlewares documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

Middlewares are created in the `src/api/middlewares.ts` file. So create the file `src/api/middlewares.ts` with the following content:

![Directory structure after adding the store middlewares file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741089625/Medusa%20Resources/quote-16_oryolz.jpg)

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares, 
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { CreateQuote } from "./store/validators"

export default defineMiddlewares({
  routes: [
    {
      method: ["POST"],
      matcher: "/store/customers/me/quotes",
      middlewares: [
        validateAndTransformBody(CreateQuote),
      ],
    },
  ],
})
```

To export the middlewares, you use the `defineMiddlewares` function. It accepts an object having a `routes` property, whose value is an array of middleware route objects. Each middleware route object has the following properties:

- `method`: The HTTP methods the middleware applies to, which is in this case `POST`.
- `matcher`: The path of the route the middleware applies to.
- `middlewares`: An array of middleware functions to apply to the route. In this case, you apply the `validateAndTransformBody` middleware, which accepts a Zod schema as a parameter and validates that a request's body matches the schema. If not, it throws and returns an error.

### Specify Quote Fields to Retrieve

In the route handler you just created, you specified what fields to retrieve in a quote using the `req.queryConfig.fields` property. The `req.queryConfig` field holds query configurations indicating the default fields to retrieve when using Query to return data in a request. This is useful to unify the returned data structure across different routes, or to allow clients to specify the fields they want to retrieve.

To add the Query configurations, you'll first create a file that exports the default fields to retrieve for a quote, then apply them in a `validateAndTransformQuery` middleware.

Learn more about configuring Query for requests in the [Request Query Configurations documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#request-query-configurations/index.html.md).

Create the file `src/api/store/customers/me/quotes/query-config.ts` with the following content:

![Directory structure after adding the query-config file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741090067/Medusa%20Resources/quote-17_n6xsdb.jpg)

```ts title="src/api/store/customers/me/quotes/query-config.ts"
export const quoteFields = [
  "id",
  "status",
  "*customer",
  "cart.id",
  "draft_order.id",
  "draft_order.currency_code",
  "draft_order.display_id",
  "draft_order.region_id",
  "draft_order.status",
  "draft_order.version",
  "draft_order.summary",
  "draft_order.total",
  "draft_order.subtotal",
  "draft_order.tax_total",
  "draft_order.order_change",
  "draft_order.discount_total",
  "draft_order.discount_tax_total",
  "draft_order.original_total",
  "draft_order.original_tax_total",
  "draft_order.item_total",
  "draft_order.item_subtotal",
  "draft_order.item_tax_total",
  "draft_order.original_item_total",
  "draft_order.original_item_subtotal",
  "draft_order.original_item_tax_total",
  "draft_order.shipping_total",
  "draft_order.shipping_subtotal",
  "draft_order.shipping_tax_total",
  "draft_order.original_shipping_tax_total",
  "draft_order.original_shipping_subtotal",
  "draft_order.original_shipping_total",
  "draft_order.created_at",
  "draft_order.updated_at",
  "*draft_order.items",
  "*draft_order.items.tax_lines",
  "*draft_order.items.adjustments",
  "*draft_order.items.variant",
  "*draft_order.items.variant.product",
  "*draft_order.items.detail",
  "*draft_order.payment_collections",
  "*order_change.actions",
]

export const retrieveStoreQuoteQueryConfig = {
  defaults: quoteFields,
  isList: false,
}

export const listStoreQuoteQueryConfig = {
  defaults: quoteFields,
  isList: true,
}
```

You export two objects:

- `retrieveStoreQuoteQueryConfig`: Specifies the default fields to retrieve for a single quote.
- `listStoreQuoteQueryConfig`: Specifies the default fields to retrieve for a list of quotes, which you'll use later.

Notice that in the fields retrieved, you specify linked records such as `customer` and `draft_order`. You can do this because you've defined links between the `Quote` data model and these data models previously.

For simplicity, this guide will apply the `listStoreQuoteQueryConfig` to all routes starting with `/store/customers/me/quotes`. However, you should instead apply `retrieveStoreQuoteQueryConfig` to routes that retrieve a single quote, and `listStoreQuoteQueryConfig` to routes that retrieve a list of quotes.

Next, you'll define a Zod schema that allows client applications to specify the fields they want to retrieve in a quote as a query parameter. In `src/api/store/validators.ts`, add the following schema:

```ts title="src/api/store/validators.ts"
// other imports...
import { createFindParams } from "@medusajs/medusa/api/utils/validators"

// ...

export type GetQuoteParamsType = z.infer<typeof GetQuoteParams>;
export const GetQuoteParams = createFindParams({
  limit: 15,
  offset: 0,
})
```

You create a `GetQuoteParams` schema using the `createFindParams` utility from Medusa. This utility creates a schema that allows clients to specify query parameters such as:

- `fields`: The fields to retrieve in a quote.
- `limit`: The maximum number of quotes to retrieve. This is useful for routes that return a list of quotes.
- `offset`: The number of quotes to skip before retrieving the next set of quotes. This is useful for routes that return a list of quotes.
- `order`: The fields to sort the quotes by either in ascending or descending order. This is useful for routes that return a list of quotes.

Finally, you'll apply these Query configurations in a middleware. So, add the following middleware in `src/api/middlewares.ts`:

```ts title="src/api/store/middlewares.ts"
// other imports...
import { GetQuoteParams } from "./store/validators"
import { validateAndTransformQuery } from "@medusajs/framework/http"
import { listStoreQuoteQueryConfig } from "./store/customers/me/quotes/query-config"

export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/store/customers/me/quotes*",
      middlewares: [
        validateAndTransformQuery(
          GetQuoteParams, 
          listStoreQuoteQueryConfig
        ),
      ],
    },
  ],
})
```

You apply the `validateAndTransformQuery` middleware on all routes starting with `/store/customers/me/quotes`. The `validateAndTransformQuery` middleware that Medusa provides accepts two parameters:

1. A Zod schema that specifies how to validate the query parameters of incoming requests.
2. A Query configuration object that specifies the default fields to retrieve in the response, which you defined in the `query-config.ts` file.

The create quote route is now ready to be used by clients to create quotes for customers.

### Test the API Route

To test out the API route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier.

#### Retrieve Publishable API Key

All requests sent to routes starting with `/store` must have a publishable API key in their header. This ensures that the request is scoped to a specific sales channel of your storefront.

To learn more about publishable API keys, refer to the [Publishable API Key documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md).

To retrieve the publishable API key from the Medusa Admin, refer to [this user guide](https://docs.medusajs.com/user-guide/settings/developer/publishable-api-keys/index.html.md).

#### Retrieve Customer Authentication Token

As mentioned before, the API route you added requires the customer to be authenticated. So, you'll first create a customer, then retrieve their authentication token to use in the request.

Before creating the customer, retrieve a registration token using the [Retrieve Registration JWT Token API route](https://docs.medusajs.com/api/store#auth_postactor_typeauth_provider_register):

```bash
curl -X POST 'http://localhost:9000/auth/customer/emailpass/register' \
-H 'Content-Type: application/json' \
--data-raw '{
  "email": "customer@gmail.com",
  "password": "supersecret"
}'
```

Make sure to replace the email and password with the credentials you want.

Then, register the customer using the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers):

```bash
curl -X POST 'http://localhost:9000/store/customers' \
-H 'Authorization: Bearer {token}' \
-H 'Content-Type: application/json' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
--data-raw '{
  "email": "customer@gmail.com"
}'
```

Make sure to replace:

- `{token}` with the registration token you received from the previous request.
- `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin.

Also, if you changed the email in the first request, make sure to change it here as well.

The customer is now registered. Lastly, you need to retrieve its authenticated token by sending a request to the [Authenticate Customer API route](https://docs.medusajs.com/api/store#auth_postactor_typeauth_provider):

```bash
curl -X POST 'http://localhost:9000/auth/customer/emailpass' \
-H 'Content-Type: application/json' \
--data-raw '{
  "email": "customer@gmail.com",
  "password": "supersecret"
}'
```

Copy the returned token to use it in the next requests.

#### Create Cart

The customer needs a cart with an item before creating the quote.

A cart requires a region ID. You can retrieve a region ID using the [List Regions API route](https://docs.medusajs.com/api/store#regions_getregions):

```bash
curl 'http://localhost:9000/store/regions' \
-H 'x-publishable-api-key: {your_publishable_api_key}'
```

Make sure to replace the `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin.

Then, create a cart for the customer using the [Create Cart API route](https://docs.medusajs.com/api/store#carts_postcarts):

```bash
curl -X POST 'http://localhost:9000/store/carts' \
-H 'Authorization: Bearer {token}' \
-H 'Content-Type: application/json' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
--data '{
    "region_id": "{region_id}"
}'
```

Make sure to replace:

- `{token}` with the authentication token you received from the previous request.
- `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin.
- `{region_id}` with the region ID you retrieved from the previous request.

This will create and return a cart. Copy its ID for the next request.

You now need to add a product variant to the cart. You can retrieve a product variant ID using the [List Products API route](https://docs.medusajs.com/api/store#products_getproducts):

```bash
curl 'http://localhost:9000/store/products' \
-H 'x-publishable-api-key: {your_publishable_api_key}'
```

Make sure to replace the `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin.

Copy the ID of a variant in a product from the response.

Finally, to add the product variant to the cart, use the [Add Item to Cart API route](https://docs.medusajs.com/api/store#carts_postcartsidlineitems):

```bash
curl -X POST 'http://localhost:9000/store/carts/{id}/line-items' \
-H 'Authorization: Bearer {token}' \
-H 'Content-Type: application/json' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
--data-raw '{
  "variant_id": "{variant_id}",
  "quantity": 1,
}'
```

Make sure to replace:

- `{id}` with the cart ID you retrieved previously.
- `{token}` with the authentication token you retrieved previously.
- `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin.
- `{variant_id}` with the product variant ID you retrieved in the previous request.

This adds the product variant to the cart. You can now use the cart to create a quote.

For more accurate totals and processing of the quote's draft order, you should:

- Add shipping and billing addresses by [updating the cart](https://docs.medusajs.com/api/store#carts_postcartsid).
- [Choose a shipping method](https://docs.medusajs.com/api/store#carts_postcartsidshippingmethods) for the cart.
- [Create a payment collection](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollections) for the cart.
- [Initialize payment session](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions) in the payment collection.

You can also learn how to build a checkout experience in a storefront by following [this storefront development guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/index.html.md). It's not specific to quote management, so you'll need to change the last step to create a quote instead of an order.

#### Create Quote

To create a quote for the customer, send a request to the `/store/customers/me/quotes` route you created:

```bash
curl -X POST 'http://localhost:9000/store/customers/me/quotes' \
-H 'Authorization: Bearer {token}' \
-H 'Content-Type: application/json' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
--data-raw '{
  "cart_id": "{cart_id}"
}'
```

Make sure to replace:

- `{token}` with the authentication token you retrieved previously.
- `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin.
- `{cart_id}` with the ID of the customer's cart.

This will create a quote for the customer and you'll receive its details in the response.

***

## Step 6: List Quotes API Route

After the customer creates a quote, the admin user needs to view these quotes to manage them. In this step, you'll create the API route to list quotes for the admin user. Then, in the next step, you'll customize the Medusa Admin dashboard to display these quotes.

The process of creating this API route will be somewhat similar to the previous route you created. You'll create the route, define the query configurations, and apply them in a middleware.

### Implement API Route

To create the API route, create the file `src/api/admin/quotes/route.ts` with the following content:

![Directory structure after adding the admin quotes route](https://res.cloudinary.com/dza7lstvk/image/upload/v1741094735/Medusa%20Resources/quote-18_uvwqt6.jpg)

```ts title="src/api/admin/quotes/route.ts" highlights={listQuotesHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: quotes, metadata } = await query.graph({
    entity: "quote",
    ...req.queryConfig,
  })

  res.json({
    quotes,
    count: metadata!.count,
    offset: metadata!.skip,
    limit: metadata!.take,
  })
}
```

You export a `GET` function in this file, which exposes a `GET` API route at `/admin/quotes`.

In the route handler function, you resolve Query from the Medusa container and use it to retrieve the list of quotes. Similar to before, you use `req.queryConfig` to specify the fields to retrieve in the response.

`req.queryConfig` also includes pagination parameters, such as `limit`, `offset`, and `count`, and they're returned in the `metadata` property of Query's result. You return the pagination details and the list of quotes in the response.

Learn more about paginating Query results in the [Query documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#apply-pagination/index.html.md).

### Add Query Configurations

Similar to before, you need to specify the default fields to retrieve in a quote and apply them in a middleware for this new route.

Since this is an admin route, create the file `src/api/admin/quotes/query-config.ts` with the following content:

![Directory structure after adding the query-config file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741095492/Medusa%20Resources/quote-19_xca6aq.jpg)

```ts title="src/api/admin/quotes/query-config.ts"
export const quoteFields = [
  "id",
  "status",
  "created_at",
  "updated_at",
  "*customer",
  "cart.id",
  "draft_order.id",
  "draft_order.currency_code",
  "draft_order.display_id",
  "draft_order.region_id",
  "draft_order.status",
  "draft_order.version",
  "draft_order.summary",
  "draft_order.total",
  "draft_order.subtotal",
  "draft_order.tax_total",
  "draft_order.order_change",
  "draft_order.discount_total",
  "draft_order.discount_tax_total",
  "draft_order.original_total",
  "draft_order.original_tax_total",
  "draft_order.item_total",
  "draft_order.item_subtotal",
  "draft_order.item_tax_total",
  "draft_order.original_item_total",
  "draft_order.original_item_subtotal",
  "draft_order.original_item_tax_total",
  "draft_order.shipping_total",
  "draft_order.shipping_subtotal",
  "draft_order.shipping_tax_total",
  "draft_order.original_shipping_tax_total",
  "draft_order.original_shipping_subtotal",
  "draft_order.original_shipping_total",
  "draft_order.created_at",
  "draft_order.updated_at",
  "*draft_order.items",
  "*draft_order.items.tax_lines",
  "*draft_order.items.adjustments",
  "*draft_order.items.variant",
  "*draft_order.items.variant.product",
  "*draft_order.items.detail",
  "*order_change.actions",
]

export const retrieveAdminQuoteQueryConfig = {
  defaults: quoteFields,
  isList: false,
}

export const listAdminQuoteQueryConfig = {
  defaults: quoteFields,
  isList: true,
}
```

You export two objects: `retrieveAdminQuoteQueryConfig` and `listAdminQuoteQueryConfig`, which specify the default fields to retrieve for a single quote and a list of quotes, respectively.

For simplicity, this guide will apply the `listAdminQuoteQueryConfig` to all routes starting with `/admin/quotes`. However, you should instead apply `retrieveAdminQuoteQueryConfig` to routes that retrieve a single quote, and `listAdminQuoteQueryConfig` to routes that retrieve a list of quotes.

Next, you'll define a Zod schema that allows client applications to specify the fields to retrieve and pagination fields as a query parameter. Create the file `src/api/admin/validators.ts` with the following content:

![Directory structure after adding the admin validators file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741095771/Medusa%20Resources/quote-20_iygrip.jpg)

```ts title="src/api/admin/validators.ts"
import {
  createFindParams,
} from "@medusajs/medusa/api/utils/validators"

export const AdminGetQuoteParams = createFindParams({
  limit: 15,
  offset: 0,
})
  .strict()
```

You define the `AdminGetQuoteParams` schema using the `createFindParams` utility from Medusa. The schema allows clients to specify query parameters such as:

- `fields`: The fields to retrieve in a quote.
- `limit`: The maximum number of quotes to retrieve.
- `offset`: The number of quotes to skip before retrieving the next set of quotes.
- `order`: The fields to sort the quotes by either in ascending or descending order.

Finally, you need to apply the `validateAndTransformQuery` middleware on this route. So, add the following to `src/api/middlewares.ts`:

```ts title="src/api/middlewares.ts"
// other imports...
import { AdminGetQuoteParams } from "./admin/quotes/validators"
import { listAdminQuoteQueryConfig } from "./admin/quotes/query-config"

export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/quotes*",
      middlewares: [
        validateAndTransformQuery(
          AdminGetQuoteParams,
          listAdminQuoteQueryConfig
        ),
      ],
    },
  ],
})
```

You add the `validateAndTransformQuery` middleware to all routes starting with `/admin/quotes`. It validates the query parameters and sets the Query configurations based on the defaults you defined and the passed query parameters.

Your API route is now ready for use. You'll test it in the next step by customizing the Medusa Admin dashboard to display the quotes.

***

## Step 7: List Quotes Route in Medusa Admin

Now that you have the API route to retrieve the list of quotes, you want to show these quotes to the admin user in the Medusa Admin dashboard. The Medusa Admin is customizable, allowing you to add new pages as UI routes.

A UI route is a React component that specifies the content to be shown in a new page in the Medusa Admin dashboard. You'll create a UI route to display the list of quotes in the Medusa Admin.

Learn more about UI routes in the [UI Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md).

### Configure JS SDK

Medusa provides a [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) that you can use to send requests to the Medusa server from any client application, including your Medusa Admin customizations.

The JS SDK is installed by default in your Medusa application. To configure it, create the file `src/admin/lib/sdk.ts` with the following content:

![Directory structure after adding the sdk file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741098137/Medusa%20Resources/quote-23_plm90s.jpg)

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

You create an instance of the JS SDK using the `Medusa` class from the `@medusajs/js-sdk` package. You pass it an object having the following properties:

- `baseUrl`: The base URL of the Medusa server.
- `debug`: A boolean indicating whether to log debug information.
- `auth`: An object specifying the authentication type. When using the JS SDK for admin customizations, you use the `session` authentication type.

### Add Admin Types

In your development, you'll need types that represents the data you'll retrieve from the Medusa server. So, create the file `src/admin/types.ts` with the following content:

![Directory structure after adding the admin type](https://res.cloudinary.com/dza7lstvk/image/upload/v1741098478/Medusa%20Resources/quote-25_jr79pa.jpg)

```ts title="src/admin/types.ts"
import {
  AdminCustomer,
  AdminOrder,
  AdminUser,
  FindParams,
  PaginatedResponse,
  StoreCart,
} from "@medusajs/framework/types"

export type AdminQuote = {
  id: string;
  status: string;
  draft_order_id: string;
  order_change_id: string;
  cart_id: string;
  customer_id: string;
  created_at: string;
  updated_at: string;
  draft_order: AdminOrder;
  cart: StoreCart;
  customer: AdminCustomer
};

export interface QuoteQueryParams extends FindParams {}

export type AdminQuotesResponse = PaginatedResponse<{
  quotes: AdminQuote[];
}>

export type AdminQuoteResponse = {
  quote: AdminQuote;
};
```

You define the following types:

- `AdminQuote`: Represents a quote.
- `QuoteQueryParams`: Represents the query parameters that can be passed when retrieving quotes.
- `AdminQuotesResponse`: Represents the response when retrieving a list of quotes.
- `AdminQuoteResponse`: Represents the response when retrieving a single quote, which you'll implement later in this guide.

You'll use these types in the rest of the customizations.

### Create useQuotes Hook

When sending requests to the Medusa server from your admin customizations, it's recommended to use [Tanstack Query](https://tanstack.com/query/latest), allowing you to benefit from its caching and data fetching capabilities.

So, you'll create a `useQuotes` hook that uses Tanstack Query and the JS SDK to fetch the list of quotes from the Medusa server.

Create the file `src/admin/hooks/quotes.tsx` with the following content:

![Directory structure after adding the hooks quotes file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741098244/Medusa%20Resources/quote-24_apdpem.jpg)

```ts title="src/admin/hooks/quotes.tsx"
import { ClientHeaders, FetchError } from "@medusajs/js-sdk"
import {
  QuoteQueryParams,
  AdminQuotesResponse,
} from "../types"
import {
  QueryKey,
  useQuery,
  UseQueryOptions,
} from "@tanstack/react-query"
import { sdk } from "../lib/sdk"

export const useQuotes = (
  query: QuoteQueryParams,
  options?: UseQueryOptions<
    AdminQuotesResponse,
    FetchError,
    AdminQuotesResponse,
    QueryKey
  >
) => {
  const fetchQuotes = (query: QuoteQueryParams, headers?: ClientHeaders) =>
    sdk.client.fetch<AdminQuotesResponse>(`/admin/quotes`, {
      query,
      headers,
    })

  const { data, ...rest } = useQuery({
    ...options,
    queryFn: () => fetchQuotes(query)!,
    queryKey: ["quote", "list"],
  })

  return { ...data, ...rest }
}
```

You define a `useQuotes` hook that accepts query parameters and optional options as a parameter. In the hook, you use the JS SDK's `client.fetch` method to retrieve the quotes from the `/admin/quotes` route.

You return the fetched data from the Medusa server. You'll use this hook in the UI route.

### Create Quotes UI Route

You can now create the UI route that will show a new page in the Medusa Admin with the list of quotes.

UI routes are created in a `page.tsx` file under the `src/admin/routes` directory. The path of the UI route is the file's path relative to `src/admin/routes`.

So, to add the UI route at `/quotes` in the Medusa Admin, create the file `src/admin/routes/quotes/page.tsx` with the following content:

![Directory structure after adding the Quotes UI route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741099122/Medusa%20Resources/quote-26_qrqzut.jpg)

```tsx title="src/admin/routes/quotes/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { DocumentText } from "@medusajs/icons"
import { 
  Container, createDataTableColumnHelper, DataTable, 
  DataTablePaginationState, Heading, Toaster, useDataTable,
} from "@medusajs/ui"
import { useNavigate } from "react-router-dom"
import { useQuotes } from "../../hooks/quotes"
import { AdminQuote } from "../../types"
import { useState } from "react"

const Quotes = () => {
  // TODO implement page content
}

export const config = defineRouteConfig({
  label: "Quotes",
  icon: DocumentText,
})

export default Quotes
```

The route file must export a React component that implements the content of the page. To show a link to the route in the sidebar, you can also export a configuration object created with `defineRouteConfig` that specifies the label and icon of the route in the Medusa Admin sidebar.

In the `Quotes` component, you'll show a table of quotes using the [DataTable component](https://docs.medusajs.com/ui/components/data-table/index.html.md) from Medusa UI. This component requires you first define the columns of the table.

To define the table's columns, add in the same file and before the `Quotes` component the following:

```tsx title="src/admin/routes/quotes/page.tsx"
const StatusTitles: Record<string, string> = {
  accepted: "Accepted",
  customer_rejected: "Customer Rejected",
  merchant_rejected: "Merchant Rejected",
  pending_merchant: "Pending Merchant",
  pending_customer: "Pending Customer",
}

const columnHelper = createDataTableColumnHelper<AdminQuote>()

const columns = [
  columnHelper.accessor("draft_order.display_id", {
    header: "ID",
  }),
  columnHelper.accessor("status", {
    header: "Status",
    cell: ({ getValue }) => StatusTitles[getValue()],
  }),
  columnHelper.accessor("customer.email", {
    header: "Email",
  }),
  columnHelper.accessor("draft_order.customer.first_name", {
    header: "First Name",
  }),
  columnHelper.accessor("draft_order.customer.company_name", {
    header: "Company Name",
  }),
  columnHelper.accessor("draft_order.total", {
    header: "Total",
    cell: ({ getValue, row }) => 
      `${row.original.draft_order.currency_code.toUpperCase()} ${getValue()}`,
  }),
  columnHelper.accessor("created_at", {
    header: "Created At",
    cell: ({ getValue }) => new Date(getValue()).toLocaleDateString(),
  }),
]
```

You use the `createDataTableColumnHelper` utility to create a function that allows you to define the columns of the table. Then, you create a `columns` array variable that defines the following columns:

1. `ID`: The display ID of the quote's draft order.
2. `Status`: The status of the quote. Here, you use an object to map the status to a human-readable title.
   - The `cell` property of the second object passed to the `columnHelper.accessor` function allows you to customize how the cell is rendered.
3. `Email`: The email of the customer.
4. `First Name`: The first name of the customer.
5. `Company Name`: The company name of the customer.
6. `Total`: The total amount of the quote's draft order. You format it to include the currency code.
7. `Created At`: The date the quote was created.

Next, you'll use these columns to render the `DataTable` component in the `Quotes` component.

Change the implementation of `Quotes` to the following:

```tsx title="src/admin/routes/quotes/page.tsx"
const Quotes = () => {
  const navigate = useNavigate()
  const [pagination, setPagination] = useState<DataTablePaginationState>({
    pageSize: 15,
    pageIndex: 0,
  })

  const {
    quotes = [],
    count,
    isPending,
  } = useQuotes({
    limit: pagination.pageSize,
    offset: pagination.pageIndex * pagination.pageSize,
    fields:
      "+draft_order.total,*draft_order.customer",
    order: "-created_at",
  })

  const table = useDataTable({
    columns,
    data: quotes,
    getRowId: (quote) => quote.id,
    rowCount: count,
    isLoading: isPending,
    pagination: {
      state: pagination,
      onPaginationChange: setPagination,
    },
    onRowClick(event, row) {
      navigate(`/quotes/${row.id}`)
    },
  })


  return (
    <>
      <Container className="flex flex-col p-0 overflow-hidden">
        <Heading className="p-6 pb-0 font-sans font-medium h1-core">
          Quotes
        </Heading>

        <DataTable instance={table}>
          <DataTable.Toolbar>
            <Heading>Products</Heading>
          </DataTable.Toolbar>
          <DataTable.Table />
          <DataTable.Pagination />
        </DataTable>
      </Container>
      <Toaster />
    </>
  )
}
```

In the component, you use the `useQuotes` hook to fetch the quotes from the Medusa server. You pass the following query parameters in the request:

- `limit` and `offset`: Pagination fields to specify the current page and the number of quotes to retrieve. These are based on the `pagination` state variable, which will be managed by the `DataTable` component.
- `fields`: The fields to retrieve in the response. You specify the total amount of the draft order and the customer of the draft order. Since you prefix the fields with `+` and `*`, the fields are retrieved along with the default fields specified in the Query configurations.
- `order`: The order in which to retrieve the quotes. Here, you retrieve the quotes in descending order of their creation date.

Next, you use the `useDataTable` hook to create a table instance with the columns you defined. You pass the fetched quotes to the `DataTable` component, along with configurations related to pagination and loading.

Notice that as part of the `useDataTable` configurations you navigate to the `/quotes/:id` UI route when a row is clicked. You'll create that route in a later step.

Finally, you render the `DataTable` component to display the quotes in a table.

### Test List Quotes UI Route

You can now test out the UI route and the route added in the previous section from the Medusa Admin.

First, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier.

You'll find a "Quotes" sidebar item. If you click on it, it will show you the table of quotes.

![Medusa Admin dashboard displaying the Quotes management interface with a data table showing quote entries including columns for quote ID, customer information, status, and creation date](https://res.cloudinary.com/dza7lstvk/image/upload/v1741099952/Medusa%20Resources/Screenshot_2025-03-04_at_4.52.17_PM_nqxyfq.png)

***

## Step 8: Retrieve Quote API Route

Next, you'll add an admin API route to retrieve a single quote. You'll use this route in the next step to add a UI route to view a quote's details. You'll later expand on that UI route to allow the admin to manage the quote.

To add the API route, create the file `src/api/admin/quotes/[id]/route.ts` with the following content:

![Directory structure after adding the single quote route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741100686/Medusa%20Resources/quote-27_ugvhbb.jpg)

```ts title="src/api/admin/quotes/[id]/route.ts"
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
  const { id } = req.params

  const {
    data: [quote],
  } = await query.graph(
    {
      entity: "quote",
      filters: { id },
      fields: req.queryConfig.fields,
    },
    { throwIfKeyNotFound: true }
  )

  res.json({ quote })
}
```

You export a `GET` route handler, which will create a `GET` API route at `/admin/quotes/:id`.

In the route handler, you resolve Query and use it to retrieve the quote. You pass the ID in the path parameter as a filter in Query. You also pass the query configuration fields, which are the same as the ones you've configured before, to retrieve the default fields and the fields specified in the query parameter.

Since you applied the middleware earlier to the `/admin/quotes*` route pattern, it will automatically apply to this route as well.

You'll test this route in the next step as you create the UI route for a single quote.

***

## Step 9: Quote Details UI Route

In the Quotes List UI route, you configured the data table to navigate to a quote's page when you click on it in the table. Now that you have the API route to retrieve a single quote, you'll create the UI route that shows a quote's details.

![Preview of the quote details page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741158359/Medusa%20Resources/Screenshot_2025-03-05_at_9.05.45_AM_wfmb5w.png)

Before you create the UI route, you need to create the hooks necessary to retrieve data from the Medusa server, and some components that will show the different elements of the page.

### Add Hooks

The first hook you'll add is a hook that will retrieve a quote using the API route you added in the previous step.

In `src/admin/hooks/quote.tsx`, add the following:

```tsx title="src/admin/hooks/quote.tsx"
// other imports...
import { AdminQuoteResponse } from "../types"

// ...

export const useQuote = (
  id: string,
  query?: QuoteQueryParams,
  options?: UseQueryOptions<
    AdminQuoteResponse,
    FetchError,
    AdminQuoteResponse,
    QueryKey
  >
) => {
  const fetchQuote = (
    id: string,
    query?: QuoteQueryParams,
    headers?: ClientHeaders
  ) =>
    sdk.client.fetch<AdminQuoteResponse>(`/admin/quotes/${id}`, {
      query,
      headers,
    })

  const { data, ...rest } = useQuery({
    queryFn: () => fetchQuote(id, query),
    queryKey: ["quote", id],
    ...options,
  })

  return { ...data, ...rest }
}
```

You define a `useQuote` hook that accepts the quote's ID and optional query parameters and options as parameters. In the hook, you use the JS SDK's `client.fetch` method to retrieve the quotes from the `/admin/quotes/:id` route.

The hook returns the fetched data from the Medusa server. You'll use this hook later in the UI route.

In addition, you'll need a hook to retrieve a preview of the quote's draft order. An order preview includes changes or edits to be applied on an order's items, such as changes in prices and quantities. Medusa already provides a [Get Order Preview API route](https://docs.medusajs.com/api/admin#orders_getordersidpreview) that you can use to retrieve the preview.

To create the hook, create the file `src/admin/hooks/order-preview.tsx` with the following content:

![Directory structure after adding the order preview hook file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741157692/Medusa%20Resources/quote-32_tb1tqw.jpg)

```tsx title="src/admin/hooks/order-preview.tsx"
import { HttpTypes } from "@medusajs/framework/types"
import { FetchError } from "@medusajs/js-sdk"
import { QueryKey, useQuery, UseQueryOptions } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"

export const orderPreviewQueryKey = "custom_orders"

export const useOrderPreview = (
  id: string,
  query?: HttpTypes.AdminOrderFilters,
  options?: Omit<
    UseQueryOptions<
      HttpTypes.AdminOrderPreviewResponse,
      FetchError,
      HttpTypes.AdminOrderPreviewResponse,
      QueryKey
    >,
    "queryFn" | "queryKey"
  >
) => {
  const { data, ...rest } = useQuery({
    queryFn: async () => sdk.admin.order.retrievePreview(id, query),
    queryKey: [orderPreviewQueryKey, id],
    ...options,
  })

  return { ...data, ...rest }
}
```

You add a `useOrderPreview` hook that accepts as parameters the order's ID, query parameters, and options. In the hook, you use the JS SDK's `admin.order.retrievePreview` method to retrieve the order preview and return it.

You'll use this hook later in the quote's details page.

### Add formatAmount Utility

In the quote's details page, you'll display the amounts of the items in the quote. To format the amounts, you'll create a utility function that formats the amount based on the currency code.

Create the file `src/admin/utils/format-amount.ts` with the following content:

![Directory structure after adding the format amount utility file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741157986/Medusa%20Resources/quote-33_k5sa9q.jpg)

```ts title="src/admin/utils/format-amount.ts"
export const formatAmount = (amount: number, currency_code: string) => {
  return new Intl.NumberFormat("en-US", {
    style: "currency",
    currency: currency_code,
  }).format(amount)
}
```

You define a `formatAmount` function that accepts an amount and a currency code as parameters. The function uses the `Intl.NumberFormat` API to format the amount as a currency based on the currency code.

You'll use this function in the UI route and its components.

### Create Amount Component

In the quote's details page, you want to display changes in amounts for items and totals. This is useful as you later add the capability to edit the price and quantity of items.

![Diagram showcasing where this component will be in the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1741183186/Medusa%20Resources/amount-highlighted_havznm.png)

To display changes in an amount, you'll create an `Amount` component and re-use it where necessary. So, create the file `src/admin/components/amount.tsx` with the following content:

![Directory structure after adding the amount component](https://res.cloudinary.com/dza7lstvk/image/upload/v1741101819/Medusa%20Resources/quote-28_iwukg2.jpg)

```tsx title="src/admin/components/amount.tsx"
import { clx } from "@medusajs/ui"
import { formatAmount } from "../utils/format-amount"

type AmountProps = {
  currencyCode: string;
  amount?: number | null;
  originalAmount?: number | null;
  align?: "left" | "right";
  className?: string;
};

export const Amount = ({
  currencyCode,
  amount,
  originalAmount,
  align = "left",
  className,
}: AmountProps) => {
  if (typeof amount === "undefined" || amount === null) {
    return (
      <div className="flex h-full w-full items-center">
        <span className="text-ui-fg-muted">-</span>
      </div>
    )
  }

  const formatted = formatAmount(amount, currencyCode)
  const originalAmountPresent = typeof originalAmount === "number"
  const originalAmountDiffers = originalAmount !== amount
  const shouldShowAmountDiff = originalAmountPresent && originalAmountDiffers

  return (
    <div
      className={clx(
        "flex h-full w-full items-center overflow-hidden",
        {
          "flex-col": shouldShowAmountDiff,
          "justify-start text-left": align === "left",
          "justify-end text-right": align === "right",
        },
        className
      )}
    >
      {shouldShowAmountDiff ? (
        <>
          <span className="truncate line-through text-xs">
            {formatAmount(originalAmount!, currencyCode)}
          </span>
          <span className="truncate text-blue-400 txt-small">{formatted}</span>
        </>
      ) : (
        <>
          <span className="truncate">{formatted}</span>
        </>
      )}
    </div>
  )
}
```

In this component, you show the current amount of an item and, if it has been changed, you show previous amount as well.

You'll use this component in other components whenever you want to display any amount that can be changed.

### Create QuoteItems Component

In the quote's UI route, you want to display the details of the items in the quote. You'll create a separate component that you'll use within the UI route.

![Screenshot showcasing where this component will be in the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1741183303/Medusa%20Resources/item-highlighted-cropped_ddyikt.png)

Create the file `src/admin/components/quote-items.tsx` with the following content:

![Directory structure after adding the quote items component](https://res.cloudinary.com/dza7lstvk/image/upload/v1741102170/Medusa%20Resources/quote-29_r5ljph.jpg)

```tsx title="src/admin/components/quote-items.tsx"
import {
  AdminOrder,
  AdminOrderLineItem,
  AdminOrderPreview,
} from "@medusajs/framework/types"
import { Badge, Text } from "@medusajs/ui"
import { useMemo } from "react"
import { Amount } from "./amount-cell"

export const QuoteItem = ({
  item,
  originalItem,
  currencyCode,
}: {
  item: AdminOrderPreview["items"][0];
  originalItem?: AdminOrderLineItem;
  currencyCode: string;
}) => {

  const isItemUpdated = useMemo(
    () => !!item.actions?.find((a) => a.action === "ITEM_UPDATE"),
    [item]
  )

  return (
    <div
      key={item.id}
      className="text-ui-fg-subtle grid grid-cols-2 items-center gap-x-4 px-6 py-4 text-left"
    >
      <div className="flex items-start gap-x-4">
        <div>
          <Text
            size="small"
            leading="compact"
            weight="plus"
            className="text-ui-fg-base"
          >
            {item.title}
          </Text>

          {item.variant_sku && (
            <div className="flex items-center gap-x-1">
              <Text size="small">{item.variant_sku}</Text>
            </div>
          )}
          <Text size="small">
            {item.variant?.options?.map((o) => o.value).join(" · ")}
          </Text>
        </div>
      </div>

      <div className="grid grid-cols-3 items-center gap-x-4">
        <div className="flex items-center justify-end gap-x-4">
          <Amount
            className="text-sm text-right justify-end items-end"
            currencyCode={currencyCode}
            // @ts-ignore
            amount={item.detail.unit_price}
            originalAmount={item.unit_price}
          />
        </div>

        <div className="flex items-center gap-x-2">
          <div className="w-fit min-w-[27px]">
            <Badge size="xsmall" color="grey">
              <span className="tabular-nums text-xs">{item.quantity}</span>x
            </Badge>
          </div>

          <div>

            {isItemUpdated && (
              <Badge
                size="2xsmall"
                rounded="full"
                color="orange"
                className="mr-1"
              >
                Modified
              </Badge>
            )}
          </div>

          <div className="overflow-visible"></div>
        </div>

        <Amount
          className="text-sm text-right justify-end items-end"
          currencyCode={currencyCode}
          amount={item.total}
          originalAmount={originalItem?.total}
        />
      </div>
    </div>
  )
}
```

You first define the component for one quote item. In the component, you show the item's title, variant SKU, and quantity. You also use the `Amount` component to show the item's current and previous amounts.

Next, add to the same file the `QuoteItems` component:

```tsx title="src/admin/components/quote-items.tsx"
export const QuoteItems = ({
  order,
  preview,
}: {
  order: AdminOrder;
  preview: AdminOrderPreview;
}) => {
  const itemsMap = useMemo(() => {
    return new Map(order.items.map((item) => [item.id, item]))
  }, [order])

  return (
    <div>
      {preview.items?.map((item) => {
        return (
          <QuoteItem
            key={item.id}
            item={item}
            originalItem={itemsMap.get(item.id)}
            currencyCode={order.currency_code}
          />
        )
      })}
    </div>
  )
}
```

In this component, you loop over the order's items and show each of them using the `QuoteItem` component.

### Create TotalsBreakdown Component

Another component you'll need in the quote's UI route is a component that breaks down the totals of the quote's draft order, such as its discount or shipping totals.

![Screenshot showcasing where this component will be in the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1741183481/Medusa%20Resources/totals-highlighted_hpxier.png)

Create the file `src/admin/components/totals-breakdown.tsx` with the following content:

![Directory structure after adding the totals breakdown component](https://res.cloudinary.com/dza7lstvk/image/upload/v1741155757/Medusa%20Resources/quote-30_de0kjq.jpg)

```tsx title="src/admin/components/totals-breakdown.tsx"
import { AdminOrder } from "@medusajs/framework/types"
import { Text } from "@medusajs/ui"
import { ReactNode } from "react"
import { formatAmount } from "../utils/format-amount"

export const Total = ({
  label,
  value,
  secondaryValue,
  tooltip,
}: {
  label: string;
  value: string | number;
  secondaryValue: string;
  tooltip?: ReactNode;
}) => (
  <div className="grid grid-cols-3 items-center">
    <Text size="small" leading="compact">
      {label} {tooltip}
    </Text>
    <div className="text-right">
      <Text size="small" leading="compact">
        {secondaryValue}
      </Text>
    </div>

    <div className="text-right">
      <Text size="small" leading="compact">
        {value}
      </Text>
    </div>
  </div>
)
```

You first define the `Total` component, which breaksdown a total item, such as discount. You'll use this component to breakdown the different totals in the `TotalsBreakdown` component.

Add the `TotalsBreakdown` component after the `Total` component:

```tsx title="src/admin/components/totals-breakdown.tsx"
export const TotalsBreakdown = ({ order }: { order: AdminOrder }) => {
  return (
    <div className="text-ui-fg-subtle flex flex-col gap-y-2 px-6 py-4">
      <Total
        label="Discounts"
        secondaryValue=""
        value={
          order.discount_total > 0
            ? `- ${formatAmount(order.discount_total, order.currency_code)}`
            : "-"
        }
      />
      {(order.shipping_methods || [])
        .sort((m1, m2) =>
          (m1.created_at as string).localeCompare(m2.created_at as string)
        )
        .map((sm, i) => {
          return (
            <div key={i}>
              <Total
                key={sm.id}
                label={"Shipping"}
                secondaryValue={sm.name}
                value={formatAmount(sm.total, order.currency_code)}
              />
            </div>
          )
        })}
    </div>
  )
}
```

In this component, you show the different totals of the quote's draft order, such as discounts and shipping totals. You use the `Total` component to show each total item.

### Create Quote Details UI Route

You can now create the UI route that will show a quote's details in the Medusa Admin.

Create the file `src/admin/routes/quote/[id]/page.tsx` with the following content:

![Diagram showcasing the directory structure after adding the quote details UI route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741157385/Medusa%20Resources/quote-31_grwlon.jpg)

```tsx title="src/admin/routes/quote/[id]/page.tsx" highlights={quotesDetailsHighlights}
import { CheckCircleSolid } from "@medusajs/icons"
import {
  Button,
  Container,
  Heading,
  Text,
  Toaster,
} from "@medusajs/ui"
import { Link, useNavigate, useParams } from "react-router-dom"
import { useOrderPreview } from "../../../hooks/order-preview"
import { 
  useQuote, 
} from "../../../hooks/quotes"
import { QuoteItems } from "../../../components/quote-items"
import { TotalsBreakdown } from "../../../components/totals-breakdown"
import { formatAmount } from "../../../utils/format-amount"

const QuoteDetails = () => {
  const { id } = useParams()
  const navigate = useNavigate()
  const { quote, isLoading } = useQuote(id!, {
    fields:
      "*draft_order.customer",
  })

  const { order: preview, isLoading: isPreviewLoading } = useOrderPreview(
    quote?.draft_order_id!,
    {},
    { enabled: !!quote?.draft_order_id }
  )

  if (isLoading || !quote) {
    return <></>
  }

  if (isPreviewLoading) {
    return <></>
  }

  if (!isPreviewLoading && !preview) {
    throw "preview not found"
  }

  // TODO render content
}

export default QuoteDetails
```

The `QuoteDetails` component will render the content of the quote's details page. So far, you retrieve the quote and its preview using the hooks you created earlier. You also render empty components or an error message if the data is still loading or not found.

To add the rendered content, replace the `TODO` with the following:

```tsx title="src/admin/routes/quote/[id]/page.tsx"
return (
  <div className="flex flex-col gap-y-3">
    <div className="flex flex-col gap-x-4 lg:flex-row xl:items-start">
      <div className="flex w-full flex-col gap-y-3">
        {quote.status === "accepted" && (
          <Container className="divide-y divide-dashed p-0">
            <div className="flex items-center justify-between px-6 py-4">
              <Text className="txt-compact-small">
                <CheckCircleSolid className="inline-block mr-2 text-green-500 text-lg" />
                Quote accepted by customer. Order is ready for processing.
              </Text>

              <Button
                size="small"
                onClick={() => navigate(`/orders/${quote.draft_order_id}`)}
              >
                View Order
              </Button>
            </div>
          </Container>
        )}

        <Container className="divide-y divide-dashed p-0">
          <div className="flex items-center justify-between px-6 py-4">
            <Heading level="h2">Quote Summary</Heading>
          </div>
          <QuoteItems order={quote.draft_order} preview={preview!} />
          <TotalsBreakdown order={quote.draft_order} />
          <div className=" flex flex-col gap-y-2 px-6 py-4">
            <div className="text-ui-fg-base flex items-center justify-between">
              <Text
                weight="plus"
                className="text-ui-fg-subtle"
                size="small"
                leading="compact"
              >
                Original Total
              </Text>
              <Text
                weight="plus"
                className="text-ui-fg-subtle"
                size="small"
                leading="compact"
              >
                {formatAmount(quote.draft_order.total, quote.draft_order.currency_code)}
              </Text>
            </div>
      
            <div className="text-ui-fg-base flex items-center justify-between">
              <Text
                className="text-ui-fg-subtle text-semibold"
                size="small"
                leading="compact"
                weight="plus"
              >
                Quote Total
              </Text>
              <Text
                className="text-ui-fg-subtle text-bold"
                size="small"
                leading="compact"
                weight="plus"
              >
                {formatAmount(preview!.summary.current_order_total, quote.draft_order.currency_code)}
              </Text>
            </div>
          </div>

          {/* TODO add actions later */}
        </Container>

      </div>

      <div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-3 xl:mt-0 xl:max-w-[400px]">
        <Container className="divide-y p-0">
          <div className="flex items-center justify-between px-6 py-4">
            <Heading level="h2">Customer</Heading>
          </div>

          <div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
            <Text size="small" weight="plus" leading="compact">
              Email
            </Text>

            <Link
              className="text-sm text-pretty text-blue-500"
              to={`/customers/${quote.draft_order?.customer?.id}`}
              onClick={(e) => e.stopPropagation()}
            >
              {quote.draft_order?.customer?.email}
            </Link>
          </div>
        </Container>
      </div>
    </div>

    <Toaster />
  </div>
)
```

You first check if the quote has been accepted by the customer, and show a banner to view the created order if so.

Next, you use the `QuoteItems` and `TotalsBreakdown` components that you created to show the quote's items and totals. You also show the original and current totals of the quote, where the original total is the total of the draft order before any changes are made to its items.

Finally, you show the customer's email and a link to view their details.

### Test Quote Details UI Route

To test the quote details UI route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier.

Next, click on Quotes in the sidebar, which will open the list of quotes UI route you created earlier. Click on one of the quotes to view its details page.

On the quote's details page, you can see the quote's items, its totals, and the customer's details. In the next steps, you'll add management features to the page.

![Quote details page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741158359/Medusa%20Resources/Screenshot_2025-03-05_at_9.05.45_AM_wfmb5w.png)

***

## Step 10: Add Merchant Reject Quote Feature

After the merchant or admin views the quote, they can choose to either reject it, send the quote back to the customer to review it, or make changes to the quote's prices and quantities.

In this step, you'll implement the functionality to reject a quote from the quote's details page. This will include:

1. Implementing the workflow to reject a quote.
2. Adding the API route to reject a quote that uses the workflow.
3. Add a hook in admin customizations that sends a request to the reject quote API route.
4. Add a button to reject the quote in the quote's details page.

### Implement Merchant Reject Quote Workflow

To reject a quote, you'll need to create a workflow that will handle the rejection process. The workflow has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the quote's details.
- [validateQuoteNotAccepted](#validateQuoteNotAccepted): Validate that the quote isn't already accepted by the customer.
- [updateQuoteStatusStep](#updateQuoteStatusStep): Update the quote's status to \`merchant\_rejected\`.

As mentioned before, the `useQueryGraphStep` is provided by Medusa's `@medusajs/medusa/core-flows` package. So, you'll only implement the remaining steps.

#### validateQuoteNotAccepted

The second step of the merchant rejection workflow ensures that a quote isn't already accepted, as it can't be rejected afterwards.

To create the step, create the file `src/workflows/steps/validate-quote-not-accepted.ts` with the following content:

![Diagram showcasing the directory structure after adding the validate quote rejection step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741159537/Medusa%20Resources/quote-34_mtcxwa.jpg)

```ts title="src/workflows/steps/validate-quote-not-accepted.ts"
import { MedusaError } from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { InferTypeOf } from "@medusajs/framework/types"
import { Quote, QuoteStatus } from "../../modules/quote/models/quote"

type StepInput = {
  quote: InferTypeOf<typeof Quote>
}

export const validateQuoteNotAccepted = createStep(
  "validate-quote-not-accepted",
  async function ({ quote }: StepInput) {
    if (quote.status === QuoteStatus.ACCEPTED) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        `Quote is already accepted by customer`
      )
    }
  }
)
```

You create a step that accepts a quote as an input and throws an error if the quote's status is `accepted`, as you can't reject a quote that has been accepted by the customer.

#### updateQuoteStatusStep

In the last step of the workflow, you'll change the workflow's status to `merchant_rejected`. So, you'll create a step that can be used to update a quote's status.

Create the file `src/workflows/steps/update-quotes.ts` with the following content:

![Diagram showcasing the directory structure after adding the update quotes step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741159754/Medusa%20Resources/quote-35_moaulz.jpg)

```ts title="src/workflows/steps/update-quotes.ts"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { QUOTE_MODULE } from "../../modules/quote"
import { QuoteStatus } from "../../modules/quote/models/quote"
import QuoteModuleService from "../../modules/quote/service"

type StepInput = {
  id: string;
  status?: QuoteStatus;
}[]

export const updateQuotesStep = createStep(
  "update-quotes",
  async (data: StepInput, { container }) => {
    const quoteModuleService: QuoteModuleService = container.resolve(
      QUOTE_MODULE
    )

    const dataBeforeUpdate = await quoteModuleService.listQuotes(
      { id: data.map((d) => d.id) }
    )

    const updatedQuotes = await quoteModuleService.updateQuotes(data)

    return new StepResponse(updatedQuotes, {
      dataBeforeUpdate,
    })
  },
  async (revertInput, { container }) => {
    if (!revertInput) {
      return
    }

    const quoteModuleService: QuoteModuleService = container.resolve(
      QUOTE_MODULE
    )

    await quoteModuleService.updateQuotes(
      revertInput.dataBeforeUpdate
    )
  }
)
```

This step accepts an array of quotes to update their status. In the step function, you resolve the Quote Module's service. Then, you retrieve the quotes' original data so that you can pass them to the compensation function. Finally, you update the quotes' data and return the updated quotes.

In the compensation function, you resolve the Quote Module's service and update the quotes with their original data.

#### Implement Workflow

You can now implement the merchant-rejection workflow. Create the file `src/workflows/merchant-reject-quote.ts` with the following content:

![Diagram showcasing the directory structure after adding the merchant reject quote workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741159969/Medusa%20Resources/quote-36_l1ffxm.jpg)

```ts title="src/workflows/merchant-reject-quote.ts" highlights={merchantRejectionWorkflowHighlights}
import { useQueryGraphStep } from "@medusajs/core-flows"
import { createWorkflow } from "@medusajs/workflows-sdk"
import { QuoteStatus } from "../modules/quote/models/quote"
import { validateQuoteNotAccepted } from "./steps/validate-quote-not-accepted"
import { updateQuotesStep } from "./steps/update-quotes"

type WorkflowInput = {
  quote_id: string;
}

export const merchantRejectQuoteWorkflow = createWorkflow(
  "merchant-reject-quote-workflow",
  (input: WorkflowInput) => {
    const { data: quotes } = useQueryGraphStep({
      entity: "quote",
      fields: ["id", "status"],
      filters: { id: input.quote_id },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    validateQuoteNotAccepted({ 
      // @ts-ignore
      quote: quotes[0],
    })

    updateQuotesStep([
      {
        id: input.quote_id,
        status: QuoteStatus.MERCHANT_REJECTED,
      },
    ])
  }
)
```

You create a workflow that accepts the ID of a quote to reject. In the workflow, you:

1. Use the `useQueryGraphStep` to retrieve the quote's details.
2. Validate that the quote isn't already accepted using the `validateQuoteNotAccepted`.
3. Update the quote's status to `merchant_rejected` using the `updateQuotesStep`.

You'll use this workflow next in an API route that allows a merchant to reject a quote.

### Add Admin Reject Quote API Route

You'll now add the API route that allows a merchant to reject a quote. The route will use the `merchantRejectQuoteWorkflow` you created in the previous step.

Create the file `src/api/admin/quotes/[id]/reject/route.ts` with the following content:

![Diagram showcasing the directory structure after adding the reject quote API route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741160251/Medusa%20Resources/quote-37_jwlfcw.jpg)

```ts title="src/api/admin/quotes/[id]/reject/route.ts"
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { merchantRejectQuoteWorkflow } from "../../../../../workflows/merchant-reject-quote"

export const POST = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
  const { id } = req.params

  await merchantRejectQuoteWorkflow(req.scope).run({
    input: {
      quote_id: id,
    },
  })

  const {
    data: [quote],
  } = await query.graph(
    {
      entity: "quote",
      filters: { id },
      fields: req.queryConfig.fields,
    },
    { throwIfKeyNotFound: true }
  )

  res.json({ quote })
}
```

You create a `POST` route handler, which will expose a `POST` API route at `/admin/quotes/:id/reject`. In the route handler, you run the `merchantRejectQuoteWorkflow` with the quote's ID as input. You then retrieve the updated quote using Query and return it in the response.

Notice that you can pass `req.queryConfig.fields` to the `query.graph` method because you've applied the `validateAndTransformQuery` middleware before to all routes starting with `/admin/quotes`.

### Add Reject Quote Hook

Now that you have the API route, you can add a React hook in the admin customizations that sends a request to the route to reject a quote.

In `src/admin/hooks/quotes.tsx` add the following new hook:

```tsx title="src/admin/hooks/quotes.tsx"
// other imports...
import {
  useMutation,
  UseMutationOptions,
} from "@tanstack/react-query"

// ...

export const useRejectQuote = (
  id: string,
  options?: UseMutationOptions<AdminQuoteResponse, FetchError, void>
) => {
  const queryClient = useQueryClient()

  const rejectQuote = async (id: string) =>
    sdk.client.fetch<AdminQuoteResponse>(`/admin/quotes/${id}/reject`, {
      method: "POST",
    })

  return useMutation({
    mutationFn: () => rejectQuote(id),
    onSuccess: (data: AdminQuoteResponse, variables: any, context: any) => {
      queryClient.invalidateQueries({
        queryKey: [orderPreviewQueryKey, id],
      })

      queryClient.invalidateQueries({
        queryKey: ["quote", id],
      })

      queryClient.invalidateQueries({
        queryKey: ["quote", "list"],
      })

      options?.onSuccess?.(data, variables, context)
    },
    ...options,
  })
}
```

You add a `useRejectQuote` hook that accepts the quote's ID and optional options as parameters. In the hook, you use the `useMutation` hook to define the mutation action that sends a request to the reject quote API route.

When the mutation is invoked, the hook sends a request to the API route to reject the quote, then invalidates all data related to the quote in the query client, which will trigger a re-fetch of the data.

### Add Reject Quote Button

Finally, you can add a button to the quote's details page that allows a merchant to reject the quote.

In `src/admin/routes/quote/[id]/page.tsx`, add the following imports:

```tsx title="src/admin/routes/quote/[id]/page.tsx"
import {
  toast,
  usePrompt,
} from "@medusajs/ui"
import { useEffect, useState } from "react"
import { 
  useRejectQuote, 
} from "../../../hooks/quotes"
```

Then, in the `QuoteDetails` component, add the following after the `useOrderPreview` hook usage:

```tsx title="src/admin/routes/quote/[id]/page.tsx"
const prompt = usePrompt()
const { mutateAsync: rejectQuote, isPending: isRejectingQuote } =
  useRejectQuote(id!)
const [showRejectQuote, setShowRejectQuote] = useState(false)

useEffect(() => {
  if (
    ["customer_rejected", "merchant_rejected", "accepted"].includes(
      quote?.status!
    )
  ) {
    setShowRejectQuote(false)
  } else {
    setShowRejectQuote(true)
  }
}, [quote])

const handleRejectQuote = async () => {
  const res = await prompt({
    title: "Reject quote?",
    description:
      "You are about to reject this customer's quote. Do you want to continue?",
    confirmText: "Continue",
    cancelText: "Cancel",
    variant: "confirmation",
  })

  if (res) {
    await rejectQuote(void 0, {
      onSuccess: () =>
        toast.success("Successfully rejected customer's quote"),
      onError: (e) => toast.error(e.message),
    })
  }
}
```

First, you initialize the following variables:

1. `prompt`: A function that you'll use to show a confirmation pop-up when the merchant tries to reject the quote. The `usePrompt` hook is available from the Medusa UI package.
2. `rejectQuote` and `isRejectingQuote`: both are returned by the `useRejectQuote` hook. The `rejectQuote` function invokes the mutation, rejecting the quote; `isRejectingQuote` is a boolean that indicates if the mutation is in progress.
3. `showRejectQuote`: A boolean that indicates whether the "Reject Quote" button should be shown. The button is shown if the quote's status is not `customer_rejected`, `merchant_rejected`, or `accepted`. This state variable is changed based on the quote's status in the `useEffect` hook.

You also define a `handleRejectQuote` function that will be called when the merchant clicks the reject quote button. The function shows a confirmation pop-up using the `prompt` function. If the user confirms the action, the function calls the `rejectQuote` function to reject the quote.

Finally, find the `TODO` in the `return` statement and replace it with the following:

```tsx title="src/admin/routes/quote/[id]/page.tsx"
<div className="bg-ui-bg-subtle flex items-center justify-end gap-x-2 rounded-b-xl px-4 py-4">
  {showRejectQuote && (
    <Button
      size="small"
      variant="secondary"
      onClick={() => handleRejectQuote()}
      disabled={isRejectingQuote}
    >
      Reject Quote
    </Button>
  )}
</div>
```

In this code snippet, you show the reject quote button if the `showRejectQuote` state is `true`. When the button is clicked, you call the `handleRejectQuote` function to reject the quote.

### Test Reject Quote Feature

To test the reject quote feature, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier.

Next, open a quote's details page. You'll find a new "Reject Quote" button. If you click on it and confirm rejecting the quote, the quote will be rejected, and a success message will be shown.

![Quote details page with reject quote button in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741161544/Medusa%20Resources/Screenshot_2025-03-05_at_9.58.41_AM_xzdv6k.png)

***

## Step 11: Add Merchant Send Quote Feature

Another action that a merchant can take on a quote is to send the quote back to the customer for review. The customer can then reject or accept the quote, which would convert it to an order.

In this step, you'll implement the functionality to send a quote back to the customer for review. This will include:

1. Implementing the workflow to send a quote back to the customer.
2. Adding the API route to send a quote back to the customer that uses the workflow.
3. Add a hook in admin customizations that sends a request to the send quote API route.
4. Add a button to send the quote back to the customer in the quote's details page.

### Implement Merchant Send Quote Workflow

You'll implement the logic of sending the quote in a workflow. The workflow has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the quote's details.
- [validateQuoteNotAccepted](#validateQuoteNotAccepted): Validate that the quote isn't already accepted by the customer.
- [updateQuoteStatusStep](#updateQuoteStatusStep): Update the quote's status to \`pending\_customer\`.

All the steps are available for use, so you can implement the workflow directly.

Create the file `src/workflows/merchant-send-quote.ts` with the following content:

![Directory structure after adding the merchant send quote workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741162342/Medusa%20Resources/quote-38_n4ksr0.jpg)

```ts title="src/workflows/merchant-send-quote.ts" highlights={sendQuoteHighlights}
import { useQueryGraphStep } from "@medusajs/core-flows"
import { createWorkflow } from "@medusajs/workflows-sdk"
import { QuoteStatus } from "../modules/quote/models/quote"
import { updateQuotesStep } from "./steps/update-quotes"
import { validateQuoteNotAccepted } from "./steps/validate-quote-not-accepted"

type WorkflowInput = {
  quote_id: string;
}

export const merchantSendQuoteWorkflow = createWorkflow(
  "merchant-send-quote-workflow",
  (input: WorkflowInput) => {
    const { data: quotes } = useQueryGraphStep({
      entity: "quote",
      fields: ["id", "status"],
      filters: { id: input.quote_id },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    validateQuoteNotAccepted({
      // @ts-ignore
      quote: quotes[0],
    })

    updateQuotesStep([
      {
        id: input.quote_id,
        status: QuoteStatus.PENDING_CUSTOMER,
      },
    ])
  }
)
```

You create a workflow that accepts the ID of a quote to send back to the customer. In the workflow, you:

1. Use the `useQueryGraphStep` to retrieve the quote's details.
2. Validate that the quote can be sent back to the customer using the `validateQuoteNotAccepted` step.
3. Update the quote's status to `pending_customer` using the `updateQuotesStep`.

You'll use this workflow next in an API route that allows a merchant to send a quote back to the customer.

### Add Send Quote API Route

You'll now add the API route that allows a merchant to send a quote back to the customer. The route will use the `merchantSendQuoteWorkflow` you created in the previous step.

Create the file `src/api/admin/quotes/[id]/send/route.ts` with the following content:

![Directory structure after adding the send quote API route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741162497/Medusa%20Resources/quote-39_us1jbh.jpg)

```ts title="src/api/admin/quotes/[id]/send/route.ts"
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { 
  merchantSendQuoteWorkflow,
} from "../../../../../workflows/merchant-send-quote"

export const POST = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
  const { id } = req.params

  await merchantSendQuoteWorkflow(req.scope).run({
    input: {
      quote_id: id,
    },
  })

  const {
    data: [quote],
  } = await query.graph(
    {
      entity: "quote",
      filters: { id },
      fields: req.queryConfig.fields,
    },
    { throwIfKeyNotFound: true }
  )

  res.json({ quote })
}
```

You create a `POST` route handler, which will expose a `POST` API route at `/admin/quotes/:id/send`. In the route handler, you run the `merchantSendQuoteWorkflow` with the quote's ID as input. You then retrieve the updated quote using Query and return it in the response.

Notice that you can pass `req.queryConfig.fields` to the `query.graph` method because you've applied the `validateAndTransformQuery` middleware before to all routes starting with `/admin/quotes`.

### Add Send Quote Hook

Now that you have the API route, you can add a React hook in the admin customizations that sends a request to the quote send API route.

In `src/admin/hooks/quotes.tsx` add the new hook:

```tsx title="src/admin/hooks/quotes.tsx"
export const useSendQuote = (
  id: string,
  options?: UseMutationOptions<AdminQuoteResponse, FetchError, void>
) => {
  const queryClient = useQueryClient()

  const sendQuote = async (id: string) =>
    sdk.client.fetch<AdminQuoteResponse>(`/admin/quotes/${id}/send`, {
      method: "POST",
    })

  return useMutation({
    mutationFn: () => sendQuote(id),
    onSuccess: (data: any, variables: any, context: any) => {
      queryClient.invalidateQueries({
        queryKey: [orderPreviewQueryKey, id],
      })

      queryClient.invalidateQueries({
        queryKey: ["quote", id],
      })

      queryClient.invalidateQueries({
        queryKey: ["quote", "list"],
      })

      options?.onSuccess?.(data, variables, context)
    },
    ...options,
  })
}
```

You add a `useSendQuote` hook that accepts the quote's ID and optional options as parameters. In the hook, you use the `useMutation` hook to define the mutation action that sends a request to the send quote API route.

When the mutation is invoked, the hook sends a request to the send quote API route, then invalidates all data related to the quote in the query client, which will trigger a re-fetch of the data.

### Add Send Quote Button

Finally, you can add a button to the quote's details page that allows a merchant to send the quote back to the customer for review.

First, add the following import to the `src/admin/routes/quote/[id]/page.tsx` file:

```tsx title="src/admin/routes/quote/[id]/page.tsx"
import { 
  useSendQuote,
} from "../../../hooks/quotes"
```

Then, after the `useRejectQuote` hook usage, add the following:

```tsx title="src/admin/routes/quote/[id]/page.tsx"
const { mutateAsync: sendQuote, isPending: isSendingQuote } = useSendQuote(
  id!
)
const [showSendQuote, setShowSendQuote] = useState(false)
```

You initialize the following variables:

1. `sendQuote` and `isSendingQuote`: Data returned by the `useSendQuote` hook. The `sendQuote` function invokes the mutation, sending the quote back to the customer; `isSendingQuote` is a boolean that indicates if the mutation is in progress.
2. `showSendQuote`: A boolean that indicates whether the "Send Quote" button should be shown.

Next, update the existing `useEffect` hook to change `showSendQuote` based on the quote's status:

```tsx title="src/admin/routes/quote/[id]/page.tsx"
useEffect(() => {
  if (["pending_merchant", "customer_rejected"].includes(quote?.status!)) {
    setShowSendQuote(true)
  } else {
    setShowSendQuote(false)
  }

  if (
    ["customer_rejected", "merchant_rejected", "accepted"].includes(
      quote?.status!
    )
  ) {
    setShowRejectQuote(false)
  } else {
    setShowRejectQuote(true)
  }
}, [quote])
```

The `useEffect` hook now updates both the `showSendQuote` and `showRejectQuote` states based on the quote's status. The "Send Quote" button is hidden if the quote's status is not `pending_merchant` or `customer_rejected`.

Then, after the `handleRejectQuote` function, add the following `handleSendQuote` function:

```tsx title="src/admin/routes/quote/[id]/page.tsx"
const handleSendQuote = async () => {
  const res = await prompt({
    title: "Send quote?",
    description:
      "You are about to send this quote to the customer. Do you want to continue?",
    confirmText: "Continue",
    cancelText: "Cancel",
    variant: "confirmation",
  })

  if (res) {
    await sendQuote(
      void 0,
      {
        onSuccess: () => toast.success("Successfully sent quote to customer"),
        onError: (e) => toast.error(e.message),
      }
    )
  }
}
```

You define a `handleSendQuote` function that will be called when the merchant clicks the "Send Quote" button. The function shows a confirmation pop-up using the `prompt` hook. If the user confirms the action, the function calls the `sendQuote` function to send the quote back to the customer.

Finally, add the following after the reject quote button in the `return` statement:

```tsx title="src/admin/routes/quote/[id]/page.tsx"
{showSendQuote && (
  <Button
    size="small"
    variant="secondary"
    onClick={() => handleSendQuote()}
    disabled={isSendingQuote || isRejectingQuote}
  >
    Send Quote
  </Button>
)}
```

In this code snippet, you show the "Send Quote" button if the `showSendQuote` state is `true`. When the button is clicked, you call the `handleSendQuote` function to send the quote back to the customer.

### Test Send Quote Feature

To test the send quote feature, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier.

Next, open a quote's details page. You'll find a new "Send Quote" button. If you click on it and confirm sending the quote, the quote will be sent back to the customer, and a success message will be shown.

You'll later add the feature to update the quote item's details before sending the quote back to the customer.

![Quote details page with send quote button in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741162950/Medusa%20Resources/Screenshot_2025-03-05_at_10.22.11_AM_sjuipg.png)

***

## Step 12: Add Customer Preview Order API Route

When the merchant sends back the quote to the customer, you want to show the customer the details of the quote and the order that would be created if they accept the quote. This helps the customer decide whether to accept or reject the quote (which you'll implement next).

In this step, you'll add the API route that allows a customer to preview a quote's order.

To create the API route, create the file `src/api/store/customers/me/quotes/[id]/preview/route.ts` with the following content:

![Directory structure after adding the customer preview order API route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741163145/Medusa%20Resources/quote-40_lmcgve.jpg)

```ts title="src/api/store/customers/me/quotes/[id]/preview/route.ts"
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const { id } = req.params
  const query = req.scope.resolve(
    ContainerRegistrationKeys.QUERY
  )

  const {
    data: [quote],
  } = await query.graph(
    {
      entity: "quote",
      filters: { id },
      fields: req.queryConfig.fields,
    },
    { throwIfKeyNotFound: true }
  )

  const orderModuleService = req.scope.resolve(
    Modules.ORDER
  )

  const preview = await orderModuleService.previewOrderChange(
    quote.draft_order_id
  )

  res.status(200).json({
    quote: {
      ...quote,
      order_preview: preview,
    },
  })
}
```

You create a `GET` route handler, which will expose a `GET` API route at `/store/customers/me/quotes/:id/preview`. In the route handler, you retrieve the quote's details using Query, then preview the order that would be created from the quote using the `previewOrderChange` method from the Order Module's service. Finally, you return the quote and its order preview in the response.

Notice that you're using the `req.queryConfig.fields` object in the `query.graph` method because you've applied the `validateAndTransformQuery` middleware before to all routes starting with `/store/customers/me/quotes`.

### Test Customer Preview Order API Route

To test the customer preview order API route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, grab the ID of a quote placed by a customer that you have their [authentication token](#retrieve-customer-authentication-token). You can find the quote ID in the URL when viewing the quote's details page in the Medusa Admin dashboard.

Finally, send the following request to get a preview of the customer's quote and order:

```bash
curl 'http://localhost:9000/store/customers/me/quotes/{quote_id}/preview' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
-H 'Authorization: Bearer {token}'
```

Make sure to replace:

- `{quote_id}` with the ID of the quote you want to preview.
- `{your_publishable_api_key}` with [your publishable API key](#retrieve-publishable-api-key).
- `{token}` with the customer's authentication token.

You'll receive in the response the quote's details with the order preview. You can show the customer these details in the storefront.

***

## Step 13: Add Customer Reject Quote Feature

After the customer previews the quote and its order, they can choose to reject the quote. When the customer rejects the quote, the quote's status is changed to `customer_rejected`. The merchant will still be able to update the quote and send it back to the customer for review.

In this step, you'll implement the functionality to reject a quote from the customer's perspective. This will include:

1. Implementing the workflow to reject a quote as a customer.
2. Adding the API route to allow customers to reject a quote using the workflow.

### Implement Customer Reject Quote Workflow

To reject a quote from the customer's perspective, you'll need to create a workflow that will handle the rejection process. The workflow has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the quote's details.
- [validateQuoteNotAccepted](#validateQuoteNotAccepted): Validate that the quote isn't already accepted by the customer.
- [updateQuoteStatusStep](#updateQuoteStatusStep): Update the quote's status to \`customer\_rejected\`.

All the steps are available for use, so you can implement the workflow directly.

Create the file `src/workflows/customer-reject-quote.ts` with the following content:

![Directory structure after adding the customer reject quote workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741164371/Medusa%20Resources/quote-41_fgpqhz.jpg)

```ts title="src/workflows/customer-reject-quote.ts" highlights={customerRejectQuoteHighlights}
import { useQueryGraphStep } from "@medusajs/core-flows"
import { createWorkflow } from "@medusajs/workflows-sdk"
import { QuoteStatus } from "../modules/quote/models/quote"
import { updateQuotesStep } from "./steps/update-quotes"
import { validateQuoteNotAccepted } from "./steps/validate-quote-not-accepted"

type WorkflowInput = {
  quote_id: string;
  customer_id: string;
}

export const customerRejectQuoteWorkflow = createWorkflow(
  "customer-reject-quote-workflow",
  (input: WorkflowInput) => {
    const { data: quotes } = useQueryGraphStep({
      entity: "quote",
      fields: ["id", "status"],
      filters: { id: input.quote_id, customer_id: input.customer_id },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    validateQuoteNotAccepted({ 
      // @ts-ignore
      quote: quotes[0],
    })

    updateQuotesStep([
      {
        id: input.quote_id,
        status: QuoteStatus.CUSTOMER_REJECTED,
      },
    ])
  }
)
```

You create a workflow that accepts the IDs of the quote to reject and the customer rejecting it. In the workflow, you:

1. Use the `useQueryGraphStep` to retrieve the quote's details. Notice that you pass the IDs of the quote and the customer as filters to ensure that the quote belongs to the customer.
2. Validate that the quote isn't already accepted using the `validateQuoteNotAccepted` step.
3. Update the quote's status to `customer_rejected` using the `updateQuotesStep`.

You'll use this workflow next in an API route that allows a customer to reject a quote.

### Add Customer Reject Quote API Route

You'll now add the API route that allows a customer to reject a quote. The route will use the `customerRejectQuoteWorkflow` you created in the previous step.

Create the file `src/api/store/customers/me/quotes/[id]/reject/route.ts` with the following content:

![Directory structure after adding the customer reject quote API route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741164538/Medusa%20Resources/quote-42_bryo2z.jpg)

```ts title="src/api/store/customers/me/quotes/[id]/reject/route.ts"
import type { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { 
  customerRejectQuoteWorkflow,
} from "../../../../../../../workflows/customer-reject-quote"

export const POST = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const { id } = req.params
  const query = req.scope.resolve(
    ContainerRegistrationKeys.QUERY
  )

  await customerRejectQuoteWorkflow(req.scope).run({
    input: {
      quote_id: id,
      customer_id: req.auth_context.actor_id,
    },
  })

  const {
    data: [quote],
  } = await query.graph(
    {
      entity: "quote",
      filters: { id },
      fields: req.queryConfig.fields,
    },
    { throwIfKeyNotFound: true }
  )

  return res.json({ quote })
}
```

You create a `POST` route handler, which will expose a `POST` API route at `/store/customers/me/quotes/:id/reject`. In the route handler, you run the `customerRejectQuoteWorkflow` with the quote's ID as input. You then retrieve the updated quote using Query and return it in the response.

Notice that you can pass `req.queryConfig.fields` to the `query.graph` method because you've applied the `validateAndTransformQuery` middleware before to all routes starting with `/store/customers/me/quotes`.

### Test Customer Reject Quote Feature

To test the customer reject quote feature, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, send a request to reject a quote for the authenticated customer:

```bash
curl -X POST 'http://localhost:9000/store/customers/me/quotes/{quote_id}/reject' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
-H 'Authorization: Bearer {token}'
```

Make sure to replace:

- `{quote_id}` with the ID of the quote you want to reject.
- `{your_publishable_api_key}` with [your publishable API key](#retrieve-publishable-api-key).
- `{token}` with the customer's [authentication token](#retrieve-customer-authentication-token).

After sending the request, the quote will be rejected, and the updated quote will be returned in the response. You can also view the quote from the Medusa Admin dashboard, where you'll find its status has changed.

***

## Step 14: Add Customer Accept Quote Feature

The customer alternatively can choose to accept a quote after previewing it. When the customer accepts a quote, the quote's draft order should become an order whose payment can be processed and items fulfilled. No further changes can be made on the quote after it's accepted.

In this step, you'll implement the functionality to allow a customer to accept a quote. This will include:

1. Implementing the workflow to accept a quote as a customer.
2. Adding the API route to allow customers to accept a quote using the workflow.

### Implement Customer Accept Quote Workflow

You'll implement the quote acceptance logic in a workflow. The workflow has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the quote's details.
- [validateQuoteCanAcceptStep](#validateQuoteCanAcceptStep): Validate that the quote can be accepted.
- [updateQuotesStep](#updateQuotesStep): Update the quote's status to \`accepted\`.
- [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md): Confirm the changes made on the draft order, such as changes to item quantities and prices.
- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md): Update the draft order to change its status and convert it into an order.

You only need to implement the `validateQuoteCanAcceptStep` step before implementing the workflow, as the other steps are already available for use.

#### validateQuoteCanAcceptStep

In the `validateQuoteCanAcceptStep`, you'll validate whether the customer can accept the quote. The customer can only accept a quote if the quote's status is `pending_customer`, meaning the merchant sent the quote back to the customer for review.

Create the file `src/workflows/steps/validate-quote-can-accept.ts` with the following content:

![Directory structure after adding the validate quote can accept step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741165829/Medusa%20Resources/quote-43_cxc3qi.jpg)

```ts title="src/workflows/steps/validate-quote-can-accept.ts"
import { MedusaError } from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { InferTypeOf } from "@medusajs/framework/types"
import { Quote, QuoteStatus } from "../../modules/quote/models/quote"

type StepInput = {
  quote: InferTypeOf<typeof Quote>
}

export const validateQuoteCanAcceptStep = createStep(
  "validate-quote-can-accept",
  async function ({ quote }: StepInput) {
    if (quote.status !== QuoteStatus.PENDING_CUSTOMER) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        `Cannot accept quote when quote status is ${quote.status}`
      )
    }
  }
)
```

You create a step that accepts a quote as input. In the step function, you throw an error if the quote's status is not `pending_customer`.

#### Implement Workflow

You can now implement the workflow that accepts a quote for a customer. Create the file `src/workflows/customer-accept-quote.ts` with the following content:

![Directory structure after adding the customer accept quote workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741166025/Medusa%20Resources/quote-44_c09ts9.jpg)

```ts title="src/workflows/customer-accept-quote.ts" highlights={customerAcceptQuoteHighlights}
import {
  confirmOrderEditRequestWorkflow,
  updateOrderWorkflow,
  useQueryGraphStep,
} from "@medusajs/core-flows"
import { OrderStatus } from "@medusajs/framework/utils"
import { createWorkflow } from "@medusajs/workflows-sdk"
import { validateQuoteCanAcceptStep } from "./steps/validate-quote-can-accept"
import { QuoteStatus } from "../modules/quote/models/quote"
import { updateQuotesStep } from "./steps/update-quotes"

type WorkflowInput = {
  quote_id: string;
  customer_id: string;
};

export const customerAcceptQuoteWorkflow = createWorkflow(
  "customer-accept-quote-workflow",
  (input: WorkflowInput) => {
    const { data: quotes } = useQueryGraphStep({
      entity: "quote",
      fields: ["id", "draft_order_id", "status"],
      filters: { id: input.quote_id, customer_id: input.customer_id },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    validateQuoteCanAcceptStep({ 
      // @ts-ignore
      quote: quotes[0],
    })

    updateQuotesStep([{ 
      id: input.quote_id, 
      status: QuoteStatus.ACCEPTED,
    }])

    confirmOrderEditRequestWorkflow.runAsStep({
      input: {
        order_id: quotes[0].draft_order_id,
        confirmed_by: input.customer_id,
      },
    })

    updateOrderWorkflow.runAsStep({
      input:{ 
        id: quotes[0].draft_order_id,
        // @ts-ignore
        status: OrderStatus.PENDING,
        is_draft_order: false,
      },
    })
  }
)
```

You create a workflow that accepts the IDs of the quote to accept and the customer accepting it. In the workflow, you:

1. Use the `useQueryGraphStep` to retrieve the quote's details. You pass the IDs of the quotes and the customer as filters to ensure that the quote belongs to the customer.
2. Validate that the quote can be accepted using the `validateQuoteCanAcceptStep`.
3. Update the quote's status to `accepted` using the `updateQuotesStep`.
4. Confirm the changes made on the draft order using the `confirmOrderEditRequestWorkflow` executed as a step. This is useful when you soon add the admin functionality to edit the quote items. Any changes that the admin has made will be applied on the draft order using this step.
5. Update the draft order to change its status and convert it into an order using the `updateOrderWorkflow` executed as a step.

You'll use this workflow next in an API route that allows a customer to accept a quote.

### Add Customer Accept Quote API Route

You'll now add the API route that allows a customer to accept a quote. The route will use the `customerAcceptQuoteWorkflow` you created in the previous step.

Create the file `src/api/store/customers/me/quotes/[id]/accept/route.ts` with the following content:

![Directory structure after adding the customer accept quote API route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741166543/Medusa%20Resources/quote-45_y8zprn.jpg)

```ts title="src/api/store/customers/me/quotes/[id]/accept/route.ts"
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { 
  customerAcceptQuoteWorkflow,
} from "../../../../../../../workflows/customer-accept-quote"

export const POST = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
  const { id } = req.params

  await customerAcceptQuoteWorkflow(req.scope).run({
    input: {
      quote_id: id,
      customer_id: req.auth_context.actor_id,
    },
  })

  const {
    data: [quote],
  } = await query.graph(
    {
      entity: "quote",
      filters: { id },
      fields: req.queryConfig.fields,
    },
    { throwIfKeyNotFound: true }
  )

  return res.json({ quote })
}
```

You create a `POST` route handler, which will expose a `POST` API route at `/store/customers/me/quotes/:id/accept`. In the route handler, you run the `customerAcceptQuoteWorkflow` with the quote's ID as input. You then retrieve the updated quote using Query and return it in the response.

Notice that you can pass `req.queryConfig.fields` to the `query.graph` method because you've applied the `validateAndTransformQuery` middleware before to all routes starting with `/store/customers/me/quotes`.

### Test Customer Accept Quote Feature

To test the customer accept quote feature, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, send a request to accept a quote for the authenticated customer:

```bash
curl -X POST 'http://localhost:9000/store/customers/me/quotes/{quote_id}/accept' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
-H 'Authorization: Bearer {token}'
```

Make sure to replace:

- `{quote_id}` with the ID of the quote you want to accept.
- `{your_publishable_api_key}` with [your publishable API key](#retrieve-publishable-api-key).
- `{token}` with the customer's [authentication token](#retrieve-customer-authentication-token).

After sending the request, the quote will be accepted, and the updated quote will be returned in the response.

You can also view the quote from the Medusa Admin dashboard, where you'll find its status has changed. The quote will also have an order, which you can view in the Orders page or using the "View Order" button on the quote's details page.

![View order button on quote's details page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741166844/Medusa%20Resources/Screenshot_2025-03-05_at_11.27.02_AM_s90rqh.png)

***

## Step 15: Edit Quote Items UI Route

The last feature you'll add is allowing merchants or admin users to make changes to the quote's items. This includes updating the item's quantity and price.

Since you're using an [order change](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) to manage edits to the quote's draft orders, you don't need to implement customizations on the server side, such as adding workflows or API routes. Instead, you'll only add a new UI route in the Medusa Admin that uses the [Order Edit API routes](https://docs.medusajs.com/api/admin#order-edits) to provide the functionality to edit the quote's items.

Order changes also allow you to add or remove items from the quote. However, for simplicity, this guide only covers how to update the item's quantity and price. Refer to the [Order Change](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) documentation to learn more.

![Edit quote items page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741169659/Medusa%20Resources/Screenshot_2025-03-05_at_12.14.05_PM_ufvkqb.png)

In this step, you'll add a new UI route to manage the quote's items. This will include:

1. Adding hooks to send requests to [Medusa's Order Edits API routes](https://docs.medusajs.com/api/admin#order-edits).
2. Implement the components you'll use within the UI route.
3. Add the new UI route to the Medusa Admin.

### Intermission: Order Editing Overview

Before you start implementing the customizations, here's a quick overview of how order editing works in Medusa.

When the admin wants to edit an order's items, Medusa creates an order change. You've already implemented this part on quote creation.

Then, when the admin makes an edit to an item, Medusa saves that edit but without applying it to the order or finalizing the edit. This allows the admin to make multiple edits before finalizing the changes.

Once the admin is finished editing, they can confirm the order edit, which finalizes it to later be applied on the order. You've already implemented applying the order edit on the order when the customer accepts the quote.

So, you still need two implement two aspects: updating the quote items, and confirming the order edit. You'll implement these in the next steps.

### Add Hooks

To implement the edit quote items functionality, you'll need two hooks:

1. A hook that updates a quote item's quantity and price using the Order Edits API routes.
2. A hook that confirms the edit of the items using the Order Edits API routes.

#### Update Quote Item Hook

The first hook updates an item's quantity and price using the Order Edits API routes. You'll use this whenever an admin updates an item's quantity or price.

In `src/admin/hooks/quotes.tsx`, add the following hook:

```tsx title="src/admin/hooks/quotes.tsx"
// other imports...
import { HttpTypes } from "@medusajs/framework/types"

// ...

export const useUpdateQuoteItem = (
  id: string,
  options?: UseMutationOptions<
    HttpTypes.AdminOrderEditPreviewResponse,
    FetchError,
    UpdateQuoteItemParams
  >
) => {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: ({
      itemId,
      ...payload
    }: UpdateQuoteItemParams) => {
      return sdk.admin.orderEdit.updateOriginalItem(id, itemId, payload)
    },
    onSuccess: (data: any, variables: any, context: any) => {
      queryClient.invalidateQueries({
        queryKey: [orderPreviewQueryKey, id],
      })

      options?.onSuccess?.(data, variables, context)
    },
    ...options,
  })
}
```

You create a `useUpdateQuoteItem` hook that accepts the quote's ID and optional options as parameters. In the hook, you use the `useMutation` hook to define the mutation action that updates an item's quantity and price using the `sdk.admin.orderEdit.updateOriginalItem` method.

When the mutation is invoked, the hook invalidates the quote's data in the query client, which will trigger a re-fetch of the data.

#### Confirm Order Edit Hook

Next, you'll add a hook that confirms the order edit. This hook will be used when the admin is done editing the quote's items. As mentioned earlier, confirming the order edit doesn't apply the changes to the order but finalizes the edit.

In `src/admin/hooks/quotes.tsx`, add the following hook:

```tsx title="src/admin/hooks/quotes.tsx"
export const useConfirmQuote = (
  id: string,
  options?: UseMutationOptions<
    HttpTypes.AdminOrderEditPreviewResponse,
    FetchError,
    void
  >
) => {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: () => sdk.admin.orderEdit.request(id),
    onSuccess: (data: any, variables: any, context: any) => {
      queryClient.invalidateQueries({
        queryKey: [orderPreviewQueryKey, id],
      })

      options?.onSuccess?.(data, variables, context)
    },
    ...options,
  })
}
```

You create a `useConfirmQuote` hook that accepts the quote's ID and optional options as parameters. In the hook, you use the `useMutation` hook to define the mutation action that confirms the order edit using the `sdk.admin.orderEdit.request` method.

When the mutation is invoked, the hook invalidates the quote's data in the query client, which will trigger a re-fetch of the data.

Now that you have the necessary hooks, you can use them in the UI route and its components.

### Add ManageItem Component

The UI route will show the list of items to the admin user and allows them to update the item's quantity and price. So, you'll create a component that allows the admin to manage a single item's details. You'll later use this component for each item in the quote.

![Screenshot of the manage item component in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741186495/Medusa%20Resources/manage-item-highlight_ouffnu.png)

Create the file `src/admin/components/manage-item.tsx` with the following content:

![Directory structure after adding the manage item component file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741168152/Medusa%20Resources/quote-46_yxanj7.jpg)

```tsx
import { AdminOrder, AdminOrderPreview } from "@medusajs/framework/types"
import {
  Badge,
  CurrencyInput,
  Hint,
  Input,
  Label,
  Text,
  toast,
} from "@medusajs/ui"
import { useMemo } from "react"
import {
  useUpdateQuoteItem,
} from "../hooks/quotes"
import { Amount } from "./amount"

type ManageItemProps = {
  originalItem: AdminOrder["items"][0];
  item: AdminOrderPreview["items"][0];
  currencyCode: string;
  orderId: string;
};

export function ManageItem({
  originalItem,
  item,
  currencyCode,
  orderId,
}: ManageItemProps) {
  const { mutateAsync: updateItem } = useUpdateQuoteItem(orderId)

  const isItemUpdated = useMemo(
    () => !!item.actions?.find((a) => a.action === "ITEM_UPDATE"),
    [item]
  )

  const onUpdate = async ({
    quantity,
    unit_price,
  }: {
    quantity?: number;
    unit_price?: number;
  }) => {
    if (
      typeof quantity === "number" &&
      quantity <= item.detail.fulfilled_quantity
    ) {
      toast.error("Quantity should be greater than the fulfilled quantity")
      return
    }

    try {
      await updateItem({
        quantity,
        unit_price,
        itemId: item.id,
      })
    } catch (e) {
      toast.error((e as any).message)
    }
  }
  
  // TODO render the item's details and input fields
}
```

You define a `ManageItem` component that accepts the following props:

- `originalItem`: The original item details from the quote. This is the item's details before any edits.
- `item`: The item's details from the quote's order preview. This is the item's details which may have been edited.
- `currencyCode`: The currency code of the quote's draft order.
- `orderId`: The ID of the quote's draft order.

In the component, you define the following variables:

- `updateItem`: The `mutateAsync` function returned by the `useUpdateQuoteItem` hook. This function updates the item's quantity and price using Medusa's Order Edits API routes.
- `isItemUpdated`: A boolean that indicates whether the item has been updated.

You also define an `onUpdate` function that will be called when the admin updates the item's quantity or price. The function sends a request to update the item's quantity and price using the `updateItem` function. If the quantity is less than or equal to the fulfilled quantity, you show an error message.

Next, you'll add a return statement to show the item's details and allow the admin to update the item's quantity and price. Replace the `TODO` with the following:

```tsx title="src/admin/components/manage-item.tsx"
return (
  <div
    key={item.quantity}
    className="bg-ui-bg-subtle shadow-elevation-card-rest my-2 rounded-xl "
  >
    <div className="flex flex-col items-center gap-x-2 gap-y-2 p-3 text-sm md:flex-row">
      <div className="flex flex-1 items-center justify-between">
        <div className="flex flex-row items-center gap-x-3">

          <div className="flex flex-col">
            <div>
              <Text className="txt-small" as="span" weight="plus">
                {item.title}{" "}
              </Text>

              {item.variant_sku && <span>({item.variant_sku})</span>}
            </div>
            <Text as="div" className="text-ui-fg-subtle txt-small">
              {item.product_title}
            </Text>
          </div>
        </div>

        {isItemUpdated && (
            <Badge
              size="2xsmall"
              rounded="full"
              color="orange"
              className="mr-1"
            >
              Modified
            </Badge>
        )}
      </div>

      <div className="flex flex-1 justify-between">
        <div className="flex flex-grow items-center gap-2">
          <Input
            className="bg-ui-bg-base txt-small w-[67px] rounded-lg [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
            type="number"
            disabled={item.detail.fulfilled_quantity === item.quantity}
            min={item.detail.fulfilled_quantity}
            defaultValue={item.quantity}
            onBlur={(e) => {
              const val = e.target.value
              const quantity = val === "" ? null : Number(val)

              if (quantity) {
                onUpdate({ quantity })
              }
            }}
          />
          <Text className="txt-small text-ui-fg-subtle">
            Quantity
          </Text>
        </div>

        <div className="text-ui-fg-subtle txt-small mr-2 flex flex-shrink-0">
          <Amount
            currencyCode={currencyCode}
            amount={item.total}
            originalAmount={originalItem?.total}
          />
        </div>
      </div>
    </div>

    <div className="grid grid-cols-1 gap-2 p-3 md:grid-cols-2">
      <div className="flex flex-col gap-y-1">
        <Label>Price</Label>
        <Hint className="!mt-1">
          Override the unit price of this product
        </Hint>
      </div>

      <div className="flex items-center gap-1">
        <div className="flex-grow">
          <CurrencyInput
            symbol={currencyCode}
              code={currencyCode}
            defaultValue={item.unit_price}
            type="numeric"
            min={0}
            onBlur={(e) => {
              onUpdate({
                unit_price: parseFloat(e.target.value),
                quantity: item.quantity,
              })
            }}
            className="bg-ui-bg-field-component hover:bg-ui-bg-field-component-hover"
          />
        </div>
      </div>
    </div>
  </div>
)
```

You show the item's title, product title, and variant SKU. If the item has been updated, you show a "Modified" badge.

You also show input fields for the quantity and price of the item, allowing the admin to update the item's quantity and price. Once the admin updates the quantity or price, the `onUpdate` function is called to send a request to update the item's details.

### Add ManageQuoteForm Component

Next, you'll add the form component that shows the list of items in the quote and allows the admin to manage each item. You'll use the `ManageItem` component you created in the previous step for each item in the quote.

![Screenshot of the manage quote form in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741186643/Medusa%20Resources/manage-quote-form-highlight_pfyee5.png)

Create the file `src/admin/components/manage-quote-form.tsx` with the following content:

![Directory structure after adding the manage quote form component file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741168581/Medusa%20Resources/quote-47_f5kamq.jpg)

```tsx title="src/admin/components/manage-quote-form.tsx"
import { AdminOrder } from "@medusajs/framework/types"
import { Button, Heading, toast } from "@medusajs/ui"
import { useConfirmQuote } from "../hooks/quotes"
import { formatAmount } from "../utils/format-amount"
import { useOrderPreview } from "../hooks/order-preview"
import { useNavigate, useParams } from "react-router-dom"
import { useMemo } from "react"
import { ManageItem } from "./manage-item"

type ReturnCreateFormProps = {
  order: AdminOrder;
};

export const ManageQuoteForm = ({ order }: ReturnCreateFormProps) => {
  const { order: preview } = useOrderPreview(order.id)
  const navigate = useNavigate()
  const { id: quoteId } = useParams()

  const { mutateAsync: confirmQuote, isPending: isRequesting } =
    useConfirmQuote(order.id)

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    try {
      await confirmQuote()
      navigate(`/quotes/${quoteId}`)

      toast.success("Successfully updated quote")
    } catch (e) {
      toast.error("Error", {
        description: (e as any).message,
      })
    }
  }
  
    const originalItemsMap = useMemo(() => {
      return new Map(order.items.map((item) => [item.id, item]))
    }, [order])

  if (!preview) {
    return <></>
  }

  // TODO render form
}
```

You define a `ManageQuoteForm` component that accepts the quote's draft order as a prop. In the component, you retrieve the preview of that order. The preview holds any edits made on the order's items.

You also define the `confirmQuote` function using the `useConfirmQuote` hook. This function confirms the order edit, finalizing the changes made on the order's items.

Then, you define the `handleSubmit` function that will be called when the admin submits the form. The function confirms the order edit using the `confirmQuote` function and navigates the admin back to the quote's details page.

Next, you'll add a return statement to show the edit form for the quote's items. Replace the `TODO` with the following:

```tsx title="src/admin/components/manage-quote-form.tsx"
return (
  <form onSubmit={handleSubmit} className="flex h-full flex-col p-4 gap-2">
    <div>
      <div className="mb-3 mt-8 flex items-center justify-between">
        <Heading level="h2">Items</Heading>
      </div>

      {preview.items.map((item) => (
        <ManageItem
          key={item.id}
          originalItem={originalItemsMap.get(item.id)!}
          item={item}
          orderId={order.id}
          currencyCode={order.currency_code}
        />
      ))}
    </div>

    <div className="mt-8 border-y border-dotted py-4">
      <div className="mb-2 flex items-center justify-between">
        <span className="txt-small text-ui-fg-subtle">
          Current Total
        </span>

        <span className="txt-small text-ui-fg-subtle">
          {formatAmount(order.total, order.currency_code)}
        </span>
      </div>

      <div className="mb-2 flex items-center justify-between">
        <span className="txt-small text-ui-fg-subtle">
          New Total
        </span>

        <span className="txt-small text-ui-fg-subtle">
          {formatAmount(preview.total, order.currency_code)}
        </span>
      </div>
    </div>

    <div className="flex w-full items-center justify-end gap-x-4">
      <div className="flex items-center justify-end gap-x-2">
        <Button
          key="submit-button"
          type="submit"
          variant="primary"
          size="small"
          disabled={isRequesting}
        >
          Confirm Edit
        </Button>
      </div>
    </div>
  </form>
)
```

You use the `ManageItem` component to show each item in the quote and allow the admin to update the item's quantity and price. You also show the updated total amount of the quote and a button to confirm the order edit.

You'll use this component next in the UI route that allows the admin to edit the quote's items.

### Implement UI Route

Finally, you'll add the UI route that allows the admin to edit the quote's items. The route will use the `ManageQuoteForm` component you created in the previous step.

Create the file `src/admin/routes/quotes/[id]/manage/page.tsx` with the following content:

![Directory structure after adding the edit quote items UI route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741168993/Medusa%20Resources/quote-48_roangs.jpg)

```tsx title="src/admin/routes/quotes/[id]/manage/page.tsx"
import { useParams } from "react-router-dom"
import { useQuote } from "../../../../hooks/quotes"
import { Container, Heading, Toaster } from "@medusajs/ui"
import { ManageQuoteForm } from "../../../../components/manage-quote-form"

const QuoteManage = () => {
  const { id } = useParams()
  const { quote, isLoading } = useQuote(id!, {
    fields:
      "*draft_order.customer",
  })

  if (isLoading) {
    return <></>
  }

  if (!quote) {
    throw "quote not found"
  }

  return (
    <>
      <Container className="divide-y p-0">
        <Heading className="flex items-center justify-between px-6 py-4">
          Manage Quote
        </Heading>

        <ManageQuoteForm order={quote.draft_order} />
      </Container>
      <Toaster />
    </>
  )
}

export default QuoteManage
```

You define a `QuoteManage` component that will show the form to manage the quote's items in the Medusa Admin dashboard.

In the component, you first retrieve the quote's details using the `useQuote` hook. Then, you show the `ManageQuoteForm` component, passing the quote's draft order as a prop.

### Add Manage Button to Quote Details Page

To allow the admin to access the manage page you just added, you'll add a new button on the quote's details page that links to the manage page.

In `src/admin/routes/quotes/[id]/page.tsx`, add the following variable definition after the `showSendQuote` variable:

```tsx title="src/admin/routes/quotes/[id]/page.tsx"
const [showManageQuote, setShowManageQuote] = useState(false)
```

This variable will be used to show or hide the manage quote button.

Then, update the existing `useEffect` hook to the following:

```tsx title="src/admin/routes/quotes/[id]/page.tsx"
useEffect(() => {
  if (["pending_merchant", "customer_rejected"].includes(quote?.status!)) {
    setShowSendQuote(true)
  } else {
    setShowSendQuote(false)
  }

  if (
    ["customer_rejected", "merchant_rejected", "accepted"].includes(
      quote?.status!
    )
  ) {
    setShowRejectQuote(false)
  } else {
    setShowRejectQuote(true)
  }

  if (![
    "pending_merchant",
    "customer_rejected",
    "merchant_rejected",
  ].includes(quote?.status!)) {
    setShowManageQuote(false)
  } else {
    setShowManageQuote(true)
  }
}, [quote])
```

The `showManageQuote` variable is now updated based on the quote's status, where you only show it if the quote is pending the merchant's action, or if it has been rejected by either the customer or merchant.

Finally, add the following button component after the `Send Quote` button:

```tsx title="src/admin/routes/quotes/[id]/page.tsx"
{showManageQuote && (
  <Button
    size="small"
    variant="secondary"
    onClick={() => navigate(`/quotes/${quote.id}/manage`)}
  >
    Manage Quote
  </Button>
)}
```

The Manage Quote button is now shown if the `showManageQuote` variable is `true`. When clicked, it navigates the admin to the manage quote page.

### Test Edit Quote Items UI Route

To test the edit quote items UI route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard at `http://localhost:9000/admin`. Open a quote's details page whose status is either `pending_merchant`, `merchant_rejected` or `customer_rejected`. You'll find a new "Manage Quote" button.

![Manage Quote button on quote's details page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741169567/Medusa%20Resources/Screenshot_2025-03-05_at_12.12.21_PM_c5fhsp.png)

Click on the button, and you'll be taken to the manage quote page where you can update the quote's items. Try to update the items' quantities or price. Then, once you're done, click the "Confirm Edit" button to finalize the changes.

![Edit quote items page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741169659/Medusa%20Resources/Screenshot_2025-03-05_at_12.14.05_PM_ufvkqb.png)

The changes can now be previewed from the quote's details page. The customer can also see these changes using the preview API route you created earlier. Once the customer accepts the quote, the changes will be applied to the order.

***

## Next Steps

You've now implemented quote management features in Medusa. There's still more that you can implement to enhance the quote management experience:

- Refer to the [B2B starter](https://github.com/medusajs/b2b-starter-medusa) for more quote-management related features, including how to add or remove items from a quote, and how to allow messages between the customer and the merchant.
- To build a storefront, refer to the [Storefront development guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/index.html.md). You can also add to the storefront features related to quote-management using the APIs you implemented in this guide.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Medusa Examples

This documentation page has examples of customizations useful for your custom development in the Medusa application.

Each section links to the associated documentation page to learn more about it.

## API Routes

An API route is a REST API endpoint that exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems.

### Create API Route

Create the file `src/api/hello-world/route.ts` with the following content:

```ts title="src/api/hello-world/route.ts"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: "[GET] Hello world!",
  })
}
```

This creates a `GET` API route at `/hello-world`.

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

### Resolve Resources in API Route

To resolve resources from the Medusa container in an API route:

```ts highlights={[["8", "resolve", "Resolve the Product Module's\nmain service from the Medusa container."]]}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest, 
  res: MedusaResponse
) => {
  const productModuleService = req.scope.resolve(
    Modules.PRODUCT
  )

  const [, count] = await productModuleService
    .listAndCountProducts()

  res.json({
    count,
  })
}
```

This resolves the Product Module's main service.

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

### Use Path Parameters

API routes can accept path parameters.

To do that, create the file `src/api/hello-world/[id]/route.ts` with the following content:

```ts title="src/api/hello-world/[id]/route.ts" highlights={singlePathHighlights}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: `[GET] Hello ${req.params.id}!`,
  })
}
```

Learn more about path parameters in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/parameters#path-parameters/index.html.md).

### Use Query Parameters

API routes can accept query parameters:

```ts highlights={queryHighlights}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: `Hello ${req.query.name}`,
  })
}
```

Learn more about query parameters in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/parameters#query-parameters/index.html.md).

### Use Body Parameters

API routes can accept request body parameters:

```ts highlights={bodyHighlights}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

type HelloWorldReq = {
  name: string
}

export const POST = async (
  req: MedusaRequest<HelloWorldReq>,
  res: MedusaResponse
) => {
  res.json({
    message: `[POST] Hello ${req.body.name}!`,
  })
}
```

Learn more about request body parameters in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/parameters#request-body-parameters/index.html.md).

### Set Response Code

You can change the response code of an API route:

```ts highlights={[["7", "status", "Change the response's status."]]}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.status(200).json({
    message: "Hello, World!",
  })
}
```

Learn more about setting the response code in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/responses#set-response-status-code/index.html.md).

### Execute a Workflow in an API Route

To execute a workflow in an API route:

```ts
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import myWorkflow from "../../workflows/hello-world"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await myWorkflow(req.scope)
    .run({
      input: {
        name: req.query.name as string,
      },
    })

  res.send(result)
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md).

### Change Response Content Type

By default, an API route's response has the content type `application/json`.

To change it to another content type, use the `writeHead` method of `MedusaResponse`:

```ts highlights={responseContentTypeHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  })

  const interval = setInterval(() => {
    res.write("Streaming data...\n")
  }, 3000)

  req.on("end", () => {
    clearInterval(interval)
    res.end()
  })
}
```

This changes the response type to return an event stream.

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/responses#change-response-content-type/index.html.md).

### Create Middleware

A middleware is a function executed when a request is sent to an API Route.

Create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import type { 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse, 
  defineMiddlewares,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      middlewares: [
        (
          req: MedusaRequest, 
          res: MedusaResponse, 
          next: MedusaNextFunction
        ) => {
          console.log("Received a request!")

          next()
        },
      ],
    },
    {
      matcher: "/custom/:id",
      middlewares: [
        (
          req: MedusaRequest, 
          res: MedusaResponse, 
          next: MedusaNextFunction
        ) => {
          console.log("With Path Parameter")

          next()
        },
      ],
    },
  ],
})
```

Learn more about middlewares in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

### Restrict HTTP Methods in Middleware

To restrict a middleware to an HTTP method:

```ts title="src/api/middlewares.ts" highlights={middlewareMethodHighlights}
import type { 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse, 
  defineMiddlewares,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      method: ["POST", "PUT"],
      middlewares: [
        // ...
      ],
    },
  ],
})
```

### Add Validation for Custom Routes

1. Create a [Zod](https://zod.dev/) schema in the file `src/api/custom/validators.ts`:

```ts title="src/api/custom/validators.ts"
import { z } from "zod"

export const PostStoreCustomSchema = z.object({
  a: z.number(),
  b: z.number(),
})
```

2. Add a validation middleware to the custom route in `src/api/middlewares.ts`:

```ts title="src/api/middlewares.ts" highlights={[["13", "validateAndTransformBody"]]}
import { 
  validateAndTransformBody,
  defineMiddlewares,
} from "@medusajs/framework/http"
import { PostStoreCustomSchema } from "./custom/validators"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom",
      method: "POST",
      middlewares: [
        validateAndTransformBody(PostStoreCustomSchema),
      ],
    },
  ],
})
```

3. Use the validated body in the `/custom` API route:

```ts title="src/api/custom/route.ts" highlights={[["14", "validatedBody"]]}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { z } from "zod"
import { PostStoreCustomSchema } from "./validators"

type PostStoreCustomSchemaType = z.infer<
  typeof PostStoreCustomSchema
>

export const POST = async (
  req: MedusaRequest<PostStoreCustomSchemaType>,
  res: MedusaResponse
) => {
  res.json({
    sum: req.validatedBody.a + req.validatedBody.b,
  })
}
```

Learn more about request body validation in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/validation/index.html.md).

### Pass Additional Data to API Route

In this example, you'll pass additional data to the Create Product API route, then consume its hook:

Find this example in details in [this documentation](https://docs.medusajs.com/docs/learn/customization/extend-features/extend-create-product/index.html.md).

1. Create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" highlights={[["10", "brand_id", "Replace with your custom field."]]}
import { defineMiddlewares } from "@medusajs/framework/http"
import { z } from "zod"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/admin/products",
      method: ["POST"],
      additionalDataValidator: {
        brand_id: z.string().optional(),
      },
    },
  ],
})
```

Learn more about additional data in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/additional-data/index.html.md).

2. Create the file `src/workflows/hooks/created-product.ts` with the following content:

```ts
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
import { StepResponse } from "@medusajs/framework/workflows-sdk"

createProductsWorkflow.hooks.productsCreated(
  (async ({ products, additional_data }, { container }) => {
    if (!additional_data.brand_id) {
      return new StepResponse([], [])
    }

    // TODO perform custom action
  }),
  (async (links, { container }) => {
    // TODO undo the action in the compensation
  })

)
```

Learn more about workflow hooks in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md).

### Restrict an API Route to Admin Users

You can protect API routes by restricting access to authenticated admin users only.

Add the following middleware in `src/api/middlewares.ts`:

```ts title="src/api/middlewares.ts" highlights={[["11", "authenticate"]]}
import { 
  defineMiddlewares,
  authenticate,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom/admin*",
      middlewares: [
        authenticate(
          "user", 
          ["session", "bearer", "api-key"]
        ),
      ],
    },
  ],
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md).

### Restrict an API Route to Logged-In Customers

You can protect API routes by restricting access to authenticated customers only.

Add the following middleware in `src/api/middlewares.ts`:

```ts title="src/api/middlewares.ts" highlights={[["11", "authenticate"]]}
import { 
  defineMiddlewares,
  authenticate,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom/customer*",
      middlewares: [
        authenticate("customer", ["session", "bearer"]),
      ],
    },
  ],
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md).

### Retrieve Logged-In Admin User

To retrieve the currently logged-in user in an API route:

Requires setting up the authentication middleware as explained in [this example](#restrict-an-api-route-to-admin-users).

```ts highlights={[["16", "req.auth_context.actor_id", "Access the user's ID."]]}
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const userModuleService = req.scope.resolve(
    Modules.USER
  )

  const user = await userModuleService.retrieveUser(
    req.auth_context.actor_id
  )

  // ...
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes#retrieve-logged-in-admin-users-details/index.html.md).

### Retrieve Logged-In Customer

To retrieve the currently logged-in customer in an API route:

Requires setting up the authentication middleware as explained in [this example](#restrict-an-api-route-to-logged-in-customers).

```ts highlights={[["18", "req.auth_context.actor_id", "Access the customer's ID."]]}
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  if (req.auth_context?.actor_id) {
    // retrieve customer
    const customerModuleService = req.scope.resolve(
      Modules.CUSTOMER
    )

    const customer = await customerModuleService.retrieveCustomer(
      req.auth_context.actor_id
    )
  }

  // ...
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes#retrieve-logged-in-customers-details/index.html.md).

### Throw Errors in API Route

To throw errors in an API route, use `MedusaError` from the Medusa Framework:

```ts highlights={[["9", "MedusaError"]]}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  if (!req.query.q) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "The `q` query parameter is required."
    )
  }

  // ...
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/errors/index.html.md).

### Override Error Handler of API Routes

To override the error handler of API routes, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" highlights={[["10", "errorHandler"]]}
import { 
  defineMiddlewares, 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"

export default defineMiddlewares({
  errorHandler: (
    error: MedusaError | any, 
    req: MedusaRequest, 
    res: MedusaResponse, 
    next: MedusaNextFunction
  ) => {
    res.status(400).json({
      error: "Something happened.",
    })
  },
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/errors#override-error-handler/index.html.md),

### Setting up CORS for Custom API Routes

By default, Medusa configures CORS for all routes starting with `/admin`, `/store`, and `/auth`.

To configure CORS for routes under other prefixes, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import type { 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse, 
  defineMiddlewares,
} from "@medusajs/framework/http"
import { ConfigModule } from "@medusajs/framework/types"
import { parseCorsOrigins } from "@medusajs/framework/utils"
import cors from "cors"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      middlewares: [
        (
          req: MedusaRequest, 
          res: MedusaResponse, 
          next: MedusaNextFunction
        ) => {
          const configModule: ConfigModule =
            req.scope.resolve("configModule")

          return cors({
            origin: parseCorsOrigins(
              configModule.projectConfig.http.storeCors
            ),
            credentials: true,
          })(req, res, next)
        },
      ],
    },
  ],
})
```

### Parse Webhook Body

By default, the Medusa application parses a request's body using JSON.

To parse a webhook's body, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" highlights={[["9"]]}
import { 
  defineMiddlewares, 
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/webhooks/*",
      bodyParser: { preserveRawBody: true },
      method: ["POST"],
    },
  ],
})
```

To access the raw body data in your route, use the `req.rawBody` property:

```ts title="src/api/webhooks/route.ts"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const POST = (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  console.log(req.rawBody)
}
```

***

## Modules

A module is a package of reusable commerce or architectural functionalities. They handle business logic in a class called a service, and define and manage data models that represent tables in the database.

### Create Module

Find this example explained in details in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).

1. Create the directory `src/modules/blog`.
2. Create the file `src/modules/blog/models/post.ts` with the following data model:

```ts title="src/modules/blog/models/post.ts"
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id().primaryKey(),
  title: model.text(),
})

export default Post
```

3. Create the file `src/modules/blog/service.ts` with the following service:

```ts title="src/modules/blog/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

class BlogModuleService extends MedusaService({
  Post,
}){
}

export default BlogModuleService
```

4. Create the file `src/modules/blog/index.ts` that exports the module definition:

```ts title="src/modules/blog/index.ts"
import BlogModuleService from "./service"
import { Module } from "@medusajs/framework/utils"

export const BLOG_MODULE = "blog"

export default Module(BLOG_MODULE, {
  service: BlogModuleService,
})
```

5. Add the module to the configurations in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    // ...
  },
  modules: [
    {
      resolve: "./modules/blog",
    },
  ],
})
```

6. Generate and run migrations:

```bash
npx medusa db:generate blog
npx medusa db:migrate
```

7. Use the module's main service in an API route:

```ts title="src/api/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import BlogModuleService from "../../modules/blog/service"
import { BLOG_MODULE } from "../../modules/blog"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
): Promise<void> {
  const blogModuleService: BlogModuleService = req.scope.resolve(
    BLOG_MODULE
  )

  const post = await blogModuleService.createPosts({
    title: "test",
  })

  res.json({
    post,
  })
}
```

### Module with Multiple Services

To add services in your module other than the main one, create them in the `services` directory of the module.

For example, create the file `src/modules/blog/services/category.ts` with the following content:

```ts title="src/modules/blog/services/category.ts"
export class CategoryService {
  // TODO add methods
}
```

Then, export the service in the file `src/modules/blog/services/index.ts`:

```ts title="src/modules/blog/services/index.ts"
export * from "./category"
```

Finally, resolve the service in your module's main service or loader:

```ts title="src/modules/blog/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"
import { CategoryService } from "./services"

type InjectedDependencies = {
  categoryService: CategoryService
}

class BlogModuleService extends MedusaService({
  Post,
}){
  private categoryService: CategoryService

  constructor({ categoryService }: InjectedDependencies) {
    super(...arguments)

    this.categoryService = categoryService
  }
}

export default BlogModuleService
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/multiple-services/index.html.md).

### Accept Module Options

A module can accept options for configurations and secrets.

To accept options in your module:

1. Pass options to the module in `medusa-config.ts`:

```ts title="medusa-config.ts" highlights={[["6", "options"]]}
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./modules/blog",
      options: {
        apiKey: true,
      },
    },
  ],
})
```

2. Access the options in the module's main service:

```ts title="src/modules/blog/service.ts" highlights={[["14", "options"]]}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

// recommended to define type in another file
type ModuleOptions = {
  apiKey?: boolean
}

export default class BlogModuleService extends MedusaService({
  Post,
}){
  protected options_: ModuleOptions

  constructor({}, options?: ModuleOptions) {
    super(...arguments)

    this.options_ = options || {
      apiKey: false,
    }
  }

  // ...
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/options/index.html.md).

### Integrate Third-Party System in Module

An example of integrating a dummy third-party system in a module's service:

```ts title="src/modules/blog/service.ts"
import { Logger } from "@medusajs/framework/types"
import { BLOG_MODULE } from ".."

export type ModuleOptions = {
  apiKey: string
}

type InjectedDependencies = {
  logger: Logger
}

export class BlogClient {
  private options_: ModuleOptions
  private logger_: Logger

  constructor(
    { logger }: InjectedDependencies, 
    options: ModuleOptions
  ) {
    this.logger_ = logger
    this.options_ = options
  }

  private async sendRequest(url: string, method: string, data?: any) {
    this.logger_.info(`Sending a ${
      method
    } request to ${url}. data: ${JSON.stringify(data, null, 2)}`)
    this.logger_.info(`Client Options: ${
      JSON.stringify(this.options_, null, 2)
    }`)
  }
}
```

Find a longer example of integrating a third-party service in [this documentation](https://docs.medusajs.com/docs/learn/customization/integrate-systems/service/index.html.md).

***

## Data Models

A data model represents a table in the database. Medusa provides a data model language to intuitively create data models.

### Create Data Model

To create a data model in a module:

This assumes you already have a module. If not, follow [this example](#create-module).

1. Create the file `src/modules/blog/models/post.ts` with the following data model:

```ts title="src/modules/blog/models/post.ts"
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id().primaryKey(),
  title: model.text(),
})

export default Post
```

2. Generate and run migrations:

```bash
npx medusa db:generate blog
npx medusa db:migrate
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md).

### Data Model Property Types

A data model can have properties of the following types:

1. ID property:

```ts
const Post = model.define("post", {
  id: model.id(),
  // ...
})
```

2. Text property:

```ts
const Post = model.define("post", {
  title: model.text(),
  // ...
})
```

3. Number property:

```ts
const Post = model.define("post", {
  views: model.number(),
  // ...
})
```

4. Big Number property:

```ts
const Post = model.define("post", {
  price: model.bigNumber(),
  // ...
})
```

5. Boolean property:

```ts
const Post = model.define("post", {
  isPublished: model.boolean(),
  // ...
})
```

6. Enum property:

```ts
const Post = model.define("post", {
  status: model.enum(["draft", "published"]),
  // ...
})
```

7. Date-Time property:

```ts
const Post = model.define("post", {
  publishedAt: model.dateTime(),
  // ...
})
```

8. JSON property:

```ts
const Post = model.define("post", {
  metadata: model.json(),
  // ...
})
```

9. Array property:

```ts
const Post = model.define("post", {
  tags: model.array(),
  // ...
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md).

### Set Primary Key

To set an `id` property as the primary key of a data model:

```ts highlights={[["4", "primaryKey"]]}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id().primaryKey(),
  // ...
})

export default Post
```

To set a `text` property as the primary key:

```ts highlights={[["4", "primaryKey"]]}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  title: model.text().primaryKey(),
  // ...
})

export default Post
```

To set a `number` property as the primary key:

```ts highlights={[["4", "primaryKey"]]}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  views: model.number().primaryKey(),
  // ...
})

export default Post
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties#set-primary-key-property/index.html.md).

### Default Property Value

To set the default value of a property:

```ts highlights={[["6"], ["9"]]}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  status: model
    .enum(["draft", "published"])
    .default("draft"),
  views: model
    .number()
    .default(0),
  // ...
})

export default Post
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties#property-default-value/index.html.md).

### Nullable Property

To allow `null` values for a property:

```ts highlights={[["4", "nullable"]]}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  price: model.bigNumber().nullable(),
  // ...
})

export default Post
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties#make-property-optional/index.html.md).

### Unique Property

To create a unique index on a property:

```ts highlights={[["4", "unique"]]}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  title: model.text().unique(),
  // ...
})

export default Post
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties#unique-property/index.html.md).

### Define Database Index on Property

To define a database index on a property:

```ts highlights={[["5", "index"]]}
import { model } from "@medusajs/framework/utils"

const MyCustom = model.define("my_custom", {
  id: model.id().primaryKey(),
  title: model.text().index(
    "IDX_POST_TITLE"
  ),
})

export default MyCustom
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties#define-database-index-on-property/index.html.md).

### Define Composite Index on Data Model

To define a composite index on a data model:

```ts highlights={[["7", "indexes"]]}
import { model } from "@medusajs/framework/utils"

const MyCustom = model.define("my_custom", {
  id: model.id().primaryKey(),
  name: model.text(),
  age: model.number().nullable(),
}).indexes([
  {
    on: ["name", "age"],
    where: {
      age: {
        $ne: null,
      },
    },
  },
])

export default MyCustom
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/index/index.html.md).

### Make a Property Searchable

To make a property searchable using terms or keywords:

```ts highlights={[["4", "searchable"]]}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  title: model.text().searchable(),
  // ...
})

export default Post
```

Then, to search by that property, pass the `q` filter to the `list` or `listAndCount` generated methods of the module's main service:

`blogModuleService` is the main service that manages the `Post` data model.

```ts
const posts = await blogModuleService.listPosts({
  q: "John",
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties#searchable-property/index.html.md).

### Create One-to-One Relationship

The following creates a one-to-one relationship between the `User` and `Email` data models:

```ts highlights={[["5", "hasOne"], ["12", "belongsTo"]]}
import { model } from "@medusajs/framework/utils"

const User = model.define("user", {
  id: model.id().primaryKey(),
  email: model.hasOne(() => Email, {
    mappedBy: "user",
  }),
})

const Email = model.define("email", {
  id: model.id().primaryKey(),
  user: model.belongsTo(() => User, {
    mappedBy: "email",
  }),
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md).

### Create One-to-Many Relationship

The following creates a one-to-many relationship between the `Store` and `Product` data models:

```ts highlights={[["5", "hasMany"], ["12", "belongsTo"]]}
import { model } from "@medusajs/framework/utils"

const Store = model.define("store", {
  id: model.id().primaryKey(),
  products: model.hasMany(() => Product, {
    mappedBy: "store",
  }),
})

const Product = model.define("product", {
  id: model.id().primaryKey(),
  store: model.belongsTo(() => Store, {
    mappedBy: "products",
  }),
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/relationships#one-to-many-relationship/index.html.md).

### Create Many-to-Many Relationship

The following creates a many-to-many relationship between the `Order` and `Product` data models:

```ts highlights={[["5", "manyToMany"], ["12", "manyToMany"]]}
import { model } from "@medusajs/framework/utils"

const Order = model.define("order", {
  id: model.id().primaryKey(),
  products: model.manyToMany(() => Product, {
    mappedBy: "orders",
  }),
})

const Product = model.define("product", {
  id: model.id().primaryKey(),
  orders: model.manyToMany(() => Order, {
    mappedBy: "products",
  }),
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md).

### Configure Cascades of Data Model

To configure cascade on a data model:

```ts highlights={[["10", "cascades"]]}
import { model } from "@medusajs/framework/utils"
import Product from "./product"

const Store = model.define("store", {
  id: model.id().primaryKey(),
  products: model.hasMany(() => Product, {
    mappedBy: "store",
  }),
})
.cascades({
  delete: ["products"],
})
```

This configures the delete cascade on the `Store` data model so that, when a store is delete, its products are also deleted.

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/relationships#cascades/index.html.md).

### Manage One-to-One Relationship

Consider you have a one-to-one relationship between `Email` and `User` data models, where an email belongs to a user.

To set the ID of the user that an email belongs to:

`blogModuleService` is the main service that manages the `Email` and `User` data models.

```ts
// when creating an email
const email = await blogModuleService.createEmails({
  // other properties...
  user: "123",
})

// when updating an email
const email = await blogModuleService.updateEmails({
  id: "321",
  // other properties...
  user: "123",
})
```

And to set the ID of a user's email when creating or updating it:

```ts
// when creating a user
const user = await blogModuleService.createUsers({
  // other properties...
  email: "123",
})

// when updating a user
const user = await blogModuleService.updateUsers({
  id: "321",
  // other properties...
  email: "123",
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/manage-relationships#manage-one-to-one-relationship/index.html.md).

### Manage One-to-Many Relationship

Consider you have a one-to-many relationship between `Product` and `Store` data models, where a store has many products.

To set the ID of the store that a product belongs to:

`blogModuleService` is the main service that manages the `Product` and `Store` data models.

```ts
// when creating a product
const product = await blogModuleService.createProducts({
  // other properties...
  store_id: "123",
})

// when updating a product
const product = await blogModuleService.updateProducts({
  id: "321",
  // other properties...
  store_id: "123",
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/manage-relationships#manage-one-to-many-relationship/index.html.md)

### Manage Many-to-Many Relationship

Consider you have a many-to-many relationship between `Order` and `Product` data models.

To set the orders a product has when creating it:

`blogModuleService` is the main service that manages the `Product` and `Order` data models.

```ts
const product = await blogModuleService.createProducts({
  // other properties...
  orders: ["123", "321"],
})
```

To add new orders to a product without removing the previous associations:

```ts
const product = await blogModuleService.retrieveProduct(
  "123",
  {
    relations: ["orders"],
  }
)

const updatedProduct = await blogModuleService.updateProducts({
  id: product.id,
  // other properties...
  orders: [
    ...product.orders.map((order) => order.id),
    "321",
  ],
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/manage-relationships#manage-many-to-many-relationship/index.html.md).

### Retrieve Related Records

To retrieve records related to a data model's records through a relation, pass the `relations` field to the `list`, `listAndCount`, or `retrieve` generated methods:

`blogModuleService` is the main service that manages the `Product` and `Order` data models.

```ts highlights={[["4", "relations"]]}
const product = await blogModuleService.retrieveProducts(
  "123",
  {
    relations: ["orders"],
  }
)
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/manage-relationships#retrieve-records-of-relation/index.html.md).

***

## Services

A service is the main resource in a module. It manages the records of your custom data models in the database, or integrate third-party systems.

### Extend Service Factory

The service factory `MedusaService` generates data-management methods for your data models.

To extend the service factory in your module's service:

```ts highlights={[["4", "MedusaService"]]}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

class BlogModuleService extends MedusaService({
  Post,
}){
  // TODO implement custom methods
}

export default BlogModuleService
```

The `BlogModuleService` will now have data-management methods for `Post`.

Refer to [this reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md) for details on the generated methods.

Learn more about the service factory in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/service-factory/index.html.md).

### Resolve Resources in the Service

To resolve resources from the module's container in a service:

### With Service Factory

```ts highlights={[["14"]]}
import { Logger } from "@medusajs/framework/types"
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

type InjectedDependencies = {
  logger: Logger
}

class BlogModuleService extends MedusaService({
  Post,
}){
  protected logger_: Logger

  constructor({ logger }: InjectedDependencies) {
    super(...arguments)
    this.logger_ = logger

    this.logger_.info("[BlogModuleService]: Hello World!")
  }

  // ...
}

export default BlogModuleService
```

### Without Service Factory

```ts highlights={[["10"]]}
import { Logger } from "@medusajs/framework/types"

type InjectedDependencies = {
  logger: Logger
}

export default class BlogModuleService {
  protected logger_: Logger

  constructor({ logger }: InjectedDependencies) {
    this.logger_ = logger

    this.logger_.info("[BlogModuleService]: Hello World!")
  }

  // ...
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md).

### Access Module Options in Service

To access options passed to a module in its service:

```ts highlights={[["14", "options"]]}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

// recommended to define type in another file
type ModuleOptions = {
  apiKey?: boolean
}

export default class BlogModuleService extends MedusaService({
  Post,
}){
  protected options_: ModuleOptions

  constructor({}, options?: ModuleOptions) {
    super(...arguments)

    this.options_ = options || {
      apiKey: "",
    }
  }

  // ...
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/options/index.html.md).

### Run Database Query in Service

To run database query in your service:

```ts highlights={[["14", "count"], ["21", "execute"]]}
// other imports...
import { 
  InjectManager,
  MedusaContext,
} from "@medusajs/framework/utils"

class BlogModuleService {
  // ...

  @InjectManager()
  async getCount(
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<number> {
    return await sharedContext.manager.count("post")
  }
  
  @InjectManager()
  async getCountSql(
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<number> {
    const data = await sharedContext.manager.execute(
      "SELECT COUNT(*) as num FROM post"
    ) 
    
    return parseInt(data[0].num)
  }
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/db-operations#run-queries/index.html.md)

### Execute Database Operations in Transactions

To execute database operations within a transaction in your service:

```ts
import { 
  InjectManager,
  InjectTransactionManager,
  MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@medusajs/framework/mikro-orm/knex"

class BlogModuleService {
  // ...
  @InjectTransactionManager()
  protected async update_(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<any> {
    const transactionManager = sharedContext.transactionManager
    await transactionManager.nativeUpdate(
      "post",
      {
        id: input.id,
      },
      {
        name: input.name,
      }
    )

    // retrieve again
    const updatedRecord = await transactionManager.execute(
      `SELECT * FROM post WHERE id = '${input.id}'`
    )

    return updatedRecord
  }

  @InjectManager()
  async update(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ) {
    return await this.update_(input, sharedContext)
  }
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/db-operations#execute-operations-in-transactions/index.html.md).

***

## Module Links

A module link forms an association between two data models of different modules, while maintaining module isolation.

### Define a Link

To define a link between your custom module and a Commerce Module, such as the Product Module:

1. Create the file `src/links/blog-product.ts` with the following content:

```ts title="src/links/blog-product.ts"
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  ProductModule.linkable.product,
  BlogModule.linkable.post
)
```

2. Run the following command to sync the links:

```bash
npx medusa db:migrate
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md).

### Define a List Link

To define a list link, where multiple records of a model can be linked to a record in another:

```ts highlights={[["9", "isList"]]}
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  ProductModule.linkable.product,
  {
    linkable: BlogModule.linkable.post,
    isList: true,
  }
)
```

Learn more about list links in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links#define-a-list-link/index.html.md).

### Set Delete Cascade on Link Definition

To ensure a model's records linked to another model are deleted when the linked model is deleted:

```ts highlights={[["9", "deleteCascades"]]}
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  ProductModule.linkable.product,
  {
    linkable: BlogModule.linkable.post,
    deleteCascades: true,
  }
)
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links#define-a-list-link/index.html.md).

### Add Custom Columns to Module Link

To add a custom column to the table that stores the linked records of two data models:

```ts highlights={[["9", "database"]]}
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  ProductModule.linkable.product,
  BlogModule.linkable.post,
  {
    database: {
      extraColumns: {
        metadata: {
          type: "json",
        },
      },
    },
  }
)
```

Then, to set the custom column when creating or updating a link between records:

```ts
await link.create({
  [Modules.PRODUCT]: {
    product_id: "123",
  },
  HELLO_MODULE: {
    my_custom_id: "321",
  },
  data: {
    metadata: {
      test: true,
    },
  },
})
```

To retrieve the custom column when retrieving linked records using Query:

```ts
import productBlogLink from "../links/product-blog"

// ...

const { data } = await query.graph({
  entity: productBlogLink.entryPoint,
  fields: ["metadata", "product.*", "post.*"],
  filters: {
    product_id: "prod_123",
  },
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/custom-columns/index.html.md).

### Create Link Between Records

To create a link between two records using Link:

```ts
import { Modules } from "@medusajs/framework/utils"
import { BLOG_MODULE } from "../../modules/blog"

// ...

await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  [HELLO_MODULE]: {
    my_custom_id: "mc_123",
  },
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link#create-link/index.html.md).

### Dismiss Link Between Records

To dismiss links between records using Link:

```ts
import { Modules } from "@medusajs/framework/utils"
import { BLOG_MODULE } from "../../modules/blog"

// ...

await link.dismiss({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  [BLOG_MODULE]: {
    post_id: "mc_123",
  },
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link#dismiss-link/index.html.md).

### Cascade Delete Linked Records

To cascade delete records linked to a deleted record:

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await productModuleService.deleteVariants([variant.id])

await link.delete({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link#cascade-delete-linked-records/index.html.md).

### Restore Linked Records

To restore records that were soft-deleted because they were linked to a soft-deleted record:

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await productModuleService.restoreProducts(["prod_123"])

await link.restore({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link#restore-linked-records/index.html.md).

***

## Query

Query fetches data across modules. It’s a set of methods registered in the Medusa container under the `query` key.

### Retrieve Records of Data Model

To retrieve records using Query in an API route:

```ts highlights={[["15", "graph"]]}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import {
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: myCustoms } = await query.graph({
    entity: "my_custom",
    fields: ["id", "name"],
  })

  res.json({ my_customs: myCustoms })
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).

### Retrieve Linked Records of Data Model

To retrieve records linked to a data model:

```ts highlights={[["20"]]}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import {
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: myCustoms } = await query.graph({
    entity: "my_custom",
    fields: [
      "id", 
      "name",
      "product.*",
    ],
  })

  res.json({ my_customs: myCustoms })
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#retrieve-linked-records/index.html.md).

### Apply Filters to Retrieved Records

To filter the retrieved records:

```ts highlights={[["18", "filters"]]}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import {
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: myCustoms } = await query.graph({
    entity: "my_custom",
    fields: ["id", "name"],
    filters: {
      id: [
        "mc_01HWSVWR4D2XVPQ06DQ8X9K7AX",
        "mc_01HWSVWK3KYHKQEE6QGS2JC3FX",
      ],
    },
  })

  res.json({ my_customs: myCustoms })
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#apply-filters/index.html.md).

### Apply Pagination and Sort Records

To paginate and sort retrieved records:

```ts highlights={[["21", "pagination"]]}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import {
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { 
    data: myCustoms,
    metadata: { count, take, skip } = {},
  } = await query.graph({
    entity: "my_custom",
    fields: ["id", "name"],
    pagination: {
      skip: 0,
      take: 10,
      order: {
        name: "DESC",
      },
    },
  })

  res.json({ 
    my_customs: myCustoms,
    count,
    take,
    skip,
  })
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#sort-records/index.html.md).

***

## Workflows

A workflow is a series of queries and actions that complete a task.

A workflow allows you to track its execution's progress, provide roll-back logic for each step to mitigate data inconsistency when errors occur, automatically retry failing steps, and more.

### Create a Workflow

To create a workflow:

1. Create the first step at `src/workflows/hello-world/steps/step-1.ts` with the following content:

```ts title="src/workflows/hello-world/steps/step-1.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

export const step1 = createStep("step-1", async () => {
  return new StepResponse(`Hello from step one!`)
})
```

2. Create the second step at `src/workflows/hello-world/steps/step-2.ts` with the following content:

```ts title="src/workflows/hello-world/steps/step-2.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

type StepInput = {
  name: string
}

export const step2 = createStep(
  "step-2", 
  async ({ name }: StepInput) => {
    return new StepResponse(`Hello ${name} from step two!`)
  }
)
```

3. Create the workflow at `src/workflows/hello-world/index.ts` with the following content:

```ts title="src/workflows/hello-world/index.ts"
import {
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { step1 } from "./steps/step-1"
import { step2 } from "./steps/step-2"

const myWorkflow = createWorkflow(
  "hello-world",
  function (input: WorkflowInput) {
    const str1 = step1()
    // to pass input
    const str2 = step2(input)

    return new WorkflowResponse({
      message: str1,
    })
  }
)

export default myWorkflow
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

### Execute a Workflow

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import myWorkflow from "../../workflows/hello-world"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await myWorkflow(req.scope)
    .run({
      input: {
        name: req.query.name as string,
      },
    })

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/customer-created.ts" highlights={[["20"], ["21"], ["22"], ["23"], ["24"], ["25"]]} collapsibleLines="1-9" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import myWorkflow from "../workflows/hello-world"
import { Modules } from "@medusajs/framework/utils"
import { IUserModuleService } from "@medusajs/framework/types"

export default async function handleCustomerCreate({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const userId = data.id
  const userModuleService: IUserModuleService = container.resolve(
    Modules.USER
  )

  const user = await userModuleService.retrieveUser(userId)

  const { result } = await myWorkflow(container)
    .run({
      input: {
        name: user.first_name,
      },
    })

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/message-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"], ["11"], ["12"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import myWorkflow from "../workflows/hello-world"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await myWorkflow(container)
    .run({
      input: {
        name: "John",
      },
    })

  console.log(result.message)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
};
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md).

### Step with a Compensation Function

Pass a compensation function that undoes what a step did as a second parameter to `createStep`:

```ts highlights={[["15"]]}
import { 
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async () => {
    const message = `Hello from step one!`

    console.log(message)

    return new StepResponse(message)
  },
  async () => {
    console.log("Oops! Rolling back my changes...")
  }
)
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/compensation-function/index.html.md).

### Manipulate Variables in Workflow

To manipulate variables within a workflow's constructor function, use `transform` from the Workflows SDK:

```ts highlights={[["14", "transform"]]}
import { 
  createWorkflow,
  WorkflowResponse,
  transform,
} from "@medusajs/framework/workflows-sdk"
// step imports...

const myWorkflow = createWorkflow(
  "hello-world", 
  function (input) {
    const str1 = step1(input)
    const str2 = step2(input)

    const str3 = transform(
      { str1, str2 },
      (data) => `${data.str1}${data.str2}`
    )

    return new WorkflowResponse(str3)
  }
)
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md)

### Using Conditions in Workflow

To perform steps or set a variable's value based on a condition, use `when-then` from the Workflows SDK:

```ts highlights={[["14", "when"]]}
import { 
  createWorkflow,
  WorkflowResponse,
  when,
} from "@medusajs/framework/workflows-sdk"
// step imports...

const workflow = createWorkflow(
  "workflow", 
  function (input: {
    is_active: boolean
  }) {

    const result = when(
      input, 
      (input) => {
        return input.is_active
      }
    ).then(() => {
      return isActiveStep()
    })

    // executed without condition
    const anotherStepResult = anotherStep(result)

    return new WorkflowResponse(
      anotherStepResult
    )
  }
)
```

### Run Workflow in Another

To run a workflow in another, use the workflow's `runAsStep` special method:

```ts highlights={[["11", "runAsStep"]]}
import {
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import { 
  createProductsWorkflow,
} from "@medusajs/medusa/core-flows"

const workflow = createWorkflow(
  "hello-world",
  async (input) => {
    const products = createProductsWorkflow.runAsStep({
      input: {
        products: [
          // ...
        ],
      },
    })

    // ...
  }
)
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/execute-another-workflow/index.html.md).

### Consume a Workflow Hook

To consume a workflow hook, create a file under `src/workflows/hooks`:

```ts title="src/workflows/hooks/product-created.ts"
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"

createProductsWorkflow.hooks.productsCreated(
  async ({ products, additional_data }, { container }) => {
    // TODO perform an action
  },
  async (dataFromStep, { container }) => {
    // undo the performed action
  }
)
```

This executes a custom step at the hook's designated point in the workflow.

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md).

### Expose a Hook

To expose a hook in a workflow, pass it in the second parameter of the returned `WorkflowResponse`:

```ts highlights={[["19", "hooks"]]}
import {
  createStep,
  createHook,
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { createProductStep } from "./steps/create-product"

export const myWorkflow = createWorkflow(
  "my-workflow", 
  function (input) {
    const product = createProductStep(input)
    const productCreatedHook = createHook(
      "productCreated", 
      { productId: product.id }
    )

    return new WorkflowResponse(product, {
      hooks: [productCreatedHook],
    })
  }
)
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/add-workflow-hook/index.html.md).

### Retry Steps

To configure steps to retry in case of errors, pass the `maxRetries` step option:

```ts highlights={[["10"]]}
import { 
  createStep, 
} from "@medusajs/framework/workflows-sdk"

export const step1 = createStep(
  {
    name: "step-1",
    maxRetries: 2,
  },
  async () => {
    console.log("Executing step 1")

    throw new Error("Oops! Something happened.")
  }
)
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/retry-failed-steps/index.html.md).

### Run Steps in Parallel

If steps in a workflow don't depend on one another, run them in parallel using `parallel` from the Workflows SDK:

```ts highlights={[["22", "parallelize"]]}
import {
  createWorkflow,
  WorkflowResponse,
  parallelize,
} from "@medusajs/framework/workflows-sdk"
import {
  createProductStep,
  getProductStep,
  createPricesStep,
  attachProductToSalesChannelStep,
} from "./steps"

interface WorkflowInput {
  title: string
}

const myWorkflow = createWorkflow(
  "my-workflow", 
  (input: WorkflowInput) => {
   const product = createProductStep(input)

   const [prices, productSalesChannel] = parallelize(
     createPricesStep(product),
     attachProductToSalesChannelStep(product)
   )

   const id = product.id
   const refetchedProduct = getProductStep(product.id)

   return new WorkflowResponse(refetchedProduct)
 }
)
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/parallel-steps/index.html.md).

### Configure Workflow Timeout

To configure the timeout of a workflow, at which the workflow's status is changed, but its execution isn't stopped, use the `timeout` configuration:

```ts highlights={[["10"]]}
import { 
  createStep,  
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
// step import...

const myWorkflow = createWorkflow({
  name: "hello-world",
  timeout: 2, // 2 seconds
}, function () {
  const str1 = step1()

  return new WorkflowResponse({
    message: str1,
  })
})

export default myWorkflow
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-timeout/index.html.md).

### Configure Step Timeout

To configure a step's timeout, at which its state changes but its execution isn't stopped, use the `timeout` property:

```ts highlights={[["4"]]}
const step1 = createStep(
  {
    name: "step-1",
    timeout: 2, // 2 seconds
  },
  async () => {
    // ...
  }
)
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-timeout#configure-step-timeout/index.html.md).

### Long-Running Workflow

A long-running workflow is a workflow that runs in the background. You can wait before executing some of its steps until another external or separate action occurs.

To create a long-running workflow, configure any of its steps to be `async` without returning any data:

```ts highlights={[["4"]]}
const step2 = createStep(
  {
    name: "step-2",
    async: true,
  },
  async () => {
    console.log("Waiting to be successful...")
  }
)
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md).

### Change Step Status in Long-Running Workflow

To change a step's status:

1. Grab the workflow's transaction ID when you run it:

```ts
const { transaction } = await myLongRunningWorkflow(req.scope)
  .run()
```

2. In an API route, workflow, or other resource, change a step's status to successful using the [Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/index.html.md):

```ts highlights={stepSuccessHighlights}
const workflowEngineService = container.resolve(
  Modules.WORKFLOW_ENGINE
)

await workflowEngineService.setStepSuccess({
  idempotencyKey: {
    action: TransactionHandlerType.INVOKE,
    transactionId,
    stepId: "step-2",
    workflowId: "hello-world",
  },
  stepResponse: new StepResponse("Done!"),
  options: {
    container,
  },
})
```

3. In an API route, workflow, or other resource, change a step's status to failure using the [Worfklow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/index.html.md):

```ts highlights={stepFailureHighlights}
const workflowEngineService = container.resolve(
  Modules.WORKFLOW_ENGINE
)

await workflowEngineService.setStepFailure({
  idempotencyKey: {
    action: TransactionHandlerType.INVOKE,
    transactionId,
    stepId: "step-2",
    workflowId: "hello-world",
  },
  stepResponse: new StepResponse("Failed!"),
  options: {
    container,
  },
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md).

### Access Long-Running Workflow's Result

Use the Workflow Engine Module's `subscribe` and `unsubscribe` methods to access the status of a long-running workflow.

For example, in an API route:

```ts highlights={[["18", "subscribe", "Subscribe to the workflow's status changes."]]}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import myWorkflow from "../../../workflows/hello-world"
import { Modules } from "@medusajs/framework/utils"

export async function GET(req: MedusaRequest, res: MedusaResponse) {
  const { transaction, result } = await myWorkflow(req.scope).run()

  const workflowEngineService = req.scope.resolve(
    Modules.WORKFLOW_ENGINE
  )

  const subscriptionOptions = {
    workflowId: "hello-world",
    transactionId: transaction.transactionId,
    subscriberId: "hello-world-subscriber",
  }

  await workflowEngineService.subscribe({
    ...subscriptionOptions,
    subscriber: async (data) => {
      if (data.eventType === "onFinish") {
        console.log("Finished execution", data.result)
        // unsubscribe
        await workflowEngineService.unsubscribe({
          ...subscriptionOptions,
          subscriberOrId: subscriptionOptions.subscriberId,
        })
      } else if (data.eventType === "onStepFailure") {
        console.log("Workflow failed", data.step)
      }
    },
  })

  res.send(result)
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow#access-long-running-workflow-status-and-result/index.html.md).

***

## Subscribers

A subscriber is a function executed whenever the event it listens to is emitted.

### Create a Subscriber

To create a subscriber that listens to the `product.created` event, create the file `src/subscribers/product-created.ts` with the following content:

```ts title="src/subscribers/product-created.ts"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"

export default async function productCreateHandler({
  event,
}: SubscriberArgs<{ id: string }>) {
  const productId = event.data.id
  console.log(`The product ${productId} was created`)
}

export const config: SubscriberConfig = {
  event: "product.created",
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).

### Resolve Resources in Subscriber

To resolve resources from the Medusa container in a subscriber, use the `container` property of its parameter:

```ts highlights={[["6", "container"], ["8", "resolve", "Resolve the Product Module's main service."]]}
import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"

export default async function productCreateHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const productModuleService = container.resolve(Modules.PRODUCT)

  const productId = data.id

  const product = await productModuleService.retrieveProduct(
    productId
  )

  console.log(`The product ${product.title} was created`)
}

export const config: SubscriberConfig = {
  event: `product.created`,
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers#resolve-resources/index.html.md).

### Send a Notification to Reset Password

To send a notification, such as an email when a user requests to reset their password, create a subscriber at `src/subscribers/handle-reset.ts` with the following content:

```ts title="src/subscribers/handle-reset.ts"
import {
  SubscriberArgs,
  type SubscriberConfig,
} from "@medusajs/medusa"
import { Modules } from "@medusajs/framework/utils"

export default async function resetPasswordTokenHandler({
  event: { data: {
    entity_id: email,
    token,
    actor_type,
  } },
  container,
}: SubscriberArgs<{ entity_id: string, token: string, actor_type: string }>) {
  const notificationModuleService = container.resolve(
    Modules.NOTIFICATION
  )

  const urlPrefix = actor_type === "customer" ? 
    "https://storefront.com" : 
    "https://admin.com"

  await notificationModuleService.createNotifications({
    to: email,
    channel: "email",
    template: "reset-password-template",
    data: {
      // a URL to a frontend application
      url: `${urlPrefix}/reset-password?token=${token}&email=${email}`,
    },
  })
}

export const config: SubscriberConfig = {
  event: "auth.password_reset",
}
```

Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/reset-password/index.html.md).

### Execute a Workflow in a Subscriber

To execute a workflow in a subscriber:

```ts
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import myWorkflow from "../workflows/hello-world"
import { Modules } from "@medusajs/framework/utils"

export default async function handleCustomerCreate({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const userId = data.id
  const userModuleService = container.resolve(
    Modules.USER
  )

  const user = await userModuleService.retrieveUser(userId)

  const { result } = await myWorkflow(container)
    .run({
      input: {
        name: user.first_name,
      },
    })

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md)

***

## Scheduled Jobs

A scheduled job is a function executed at a specified interval of time in the background of your Medusa application.

### Create a Scheduled Job

To create a scheduled job, create the file `src/jobs/hello-world.ts` with the following content:

```ts title="src/jobs/hello-world.ts"
// the scheduled-job function
export default function () {
  console.log("Time to say hello world!")
}

// the job's configurations
export const config = {
  name: "every-minute-message",
  // execute every minute
  schedule: "* * * * *",
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md).

### Resolve Resources in Scheduled Job

To resolve resources in a scheduled job, use the `container` accepted as a first parameter:

```ts highlights={[["5", "container"], ["7", "resolve", "Resolve the Product Module's main service."]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const productModuleService = container.resolve(Modules.PRODUCT)

  const [, count] = await productModuleService.listAndCountProducts()

  console.log(
    `Time to check products! You have ${count} product(s)`
  )
}

export const config = {
  name: "every-minute-message",
  // execute every minute
  schedule: "* * * * *",
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs#resolve-resources/index.html.md)

### Specify a Job's Execution Number

To limit the scheduled job's execution to a number of times during the Medusa application's runtime, use the `numberOfExecutions` configuration:

```ts highlights={[["9", "numberOfExecutions"]]}
export default async function myCustomJob() {
  console.log("I'll be executed three times only.")
}

export const config = {
  name: "hello-world",
  // execute every minute
  schedule: "* * * * *",
  numberOfExecutions: 3,
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/execution-number/index.html.md).

### Execute a Workflow in a Scheduled Job

To execute a workflow in a scheduled job:

```ts
import { MedusaContainer } from "@medusajs/framework/types"
import myWorkflow from "../workflows/hello-world"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await myWorkflow(container)
    .run({
      input: {
        name: "John",
      },
    })

  console.log(result.message)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md)

***

## Loaders

A loader is a function defined in a module that's executed when the Medusa application starts.

### Create a Loader

To create a loader, add it to a module's `loaders` directory.

For example, create the file `src/modules/hello/loaders/hello-world.ts` with the following content:

```ts title="src/modules/hello/loaders/hello-world.ts"
export default async function helloWorldLoader() {
  console.log(
    "[HELLO MODULE] Just started the Medusa application!"
  )
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/loaders/index.html.md).

### Resolve Resources in Loader

To resolve resources in a loader, use the `container` property of its first parameter:

```ts highlights={[["9", "container"], ["11", "resolve", "Resolve the Logger from the module's container."]]}
import {
  LoaderOptions,
} from "@medusajs/framework/types"
import { 
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export default async function helloWorldLoader({
  container,
}: LoaderOptions) {
  const logger = container.resolve(ContainerRegistrationKeys.LOGGER)

  logger.info("[helloWorldLoader]: Hello, World!")
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md).

### Access Module Options

To access a module's options in its loader, use the `options` property of its first parameter:

```ts highlights={[["11", "options"]]}
import {
  LoaderOptions,
} from "@medusajs/framework/types"

// recommended to define type in another file
type ModuleOptions = {
  apiKey?: boolean
}

export default async function helloWorldLoader({
  options,
}: LoaderOptions<ModuleOptions>) {
  
  console.log(
    "[HELLO MODULE] Just started the Medusa application!",
    options
  )
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/options/index.html.md).

### Register Resources in the Module's Container

To register a resource in the Module's container using a loader, use the `container`'s `registerAdd` method:

```ts highlights={[["9", "registerAdd"]]}
import {
  LoaderOptions,
} from "@medusajs/framework/types"
import { asValue } from "@medusajs/framework/awilix"

export default async function helloWorldLoader({
  container,
}: LoaderOptions) {
  container.registerAdd(
    "custom_data",
    asValue({
      test: true,
    })
  )
}
```

Where the first parameter of `registerAdd` is the name to register the resource under, and the second parameter is the resource to register.

***

## Admin Customizations

You can customize the Medusa Admin to inject widgets in existing pages, or create new pages using UI routes.

For a list of components to use in the admin dashboard, refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/index.html.md).

### Create Widget

A widget is a React component that can be injected into an existing page in the admin dashboard.

To create a widget in the admin dashboard, create the file `src/admin/widgets/products-widget.tsx` with the following content:

```tsx title="src/admin/widgets/products-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

const ProductWidget = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">Product Widget</Heading>
      </div>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.list.before",
})

export default ProductWidget
```

Learn more about widgets in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md).

### Receive Details Props in Widgets

Widgets created in a details page, such as widgets in the `product.details.before` injection zone, receive a prop of the data of the details page (for example, the product):

```tsx highlights={[["10", "data"]]}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"
import { 
  DetailWidgetProps, 
  AdminProduct,
} from "@medusajs/framework/types"

// The widget
const ProductWidget = ({ 
  data,
}: DetailWidgetProps<AdminProduct>) => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">
          Product Widget {data.title}
        </Heading>
      </div>
    </Container>
  )
}

// The widget's configurations
export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets#detail-widget-props/index.html.md).

### Create a UI Route

A UI route is a React Component that adds a new page to your admin dashboard. The UI Route can be shown in the sidebar or added as a nested page.

To create a UI route in the admin dashboard, create the file `src/admin/routes/custom/page.tsx` with the following content:

```tsx title="src/admin/routes/custom/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { Container, Heading } from "@medusajs/ui"

const CustomPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">This is my custom route</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Custom Route",
  icon: ChatBubbleLeftRight,
})

export default CustomPage
```

This adds a new page at `localhost:9000/app/custom`.

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md).

### Create Settings Page

To create a settings page, create a UI route under the `src/admin/routes/settings` directory.

For example, create the file `src/admin/routes/settings/custom/page.tsx` with the following content:

```tsx title="src/admin/routes/settings/custom/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

const CustomSettingPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h1">Custom Setting Page</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Custom",
})

export default CustomSettingPage
```

This adds a setting page at `localhost:9000/app/settings/custom`.

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes#create-settings-page/index.html.md)

### Accept Path Parameters in UI Routes

To accept a path parameter in a UI route, name one of the directories in its path in the format `[param]`.

For example, create the file `src/admin/routes/custom/[id]/page.tsx` with the following content:

```tsx title="src/admin/routes/custom/[id]/page.tsx"
import { useParams } from "react-router-dom"
import { Container } from "@medusajs/ui"

const CustomPage = () => {
  const { id } = useParams()

  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h1">Passed ID: {id}</Heading>
      </div>
    </Container>
  )
}

export default CustomPage
```

This creates a UI route at `localhost:9000/app/custom/:id`, where `:id` is a path parameter.

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes#path-parameters/index.html.md)

### Send Request to API Route

To send a request to custom API routes from the admin dashboard, use the Fetch API.

For example:

```tsx
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container } from "@medusajs/ui"
import { useEffect, useState } from "react"

const ProductWidget = () => {
  const [productsCount, setProductsCount] = useState(0)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    if (!loading) {
      return
    }

    fetch(`/admin/products`, {
      credentials: "include",
    })
    .then((res) => res.json())
    .then(({ count }) => {
      setProductsCount(count)
      setLoading(false)
    })
  }, [loading])

  return (
    <Container className="divide-y p-0">
      {loading && <span>Loading...</span>}
      {!loading && <span>You have {productsCount} Product(s).</span>}
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.list.before",
})

export default ProductWidget
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md)

### Add Link to Another Page

To add a link to another page in a UI route or a widget, use `react-router-dom`'s `Link` component:

```tsx
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container } from "@medusajs/ui"
import { Link } from "react-router-dom"

// The widget
const ProductWidget = () => {
  return (
    <Container className="divide-y p-0">
      <Link to={"/orders"}>View Orders</Link>
    </Container>
  )
}

// The widget's configurations
export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/tips#routing-functionalities/index.html.md).

***

## Integration Tests

Medusa provides a `@medusajs/test-utils` package with utility tools to create integration tests for your custom API routes, modules, or other Medusa customizations.

For details on setting up your project for integration tests, refer to [this documentation](https://docs.medusajs.com/docs/learn/debugging-and-testing/testing-tools/index.html.md).

### Test Custom API Route

To create a test for a custom API route, create the file `integration-tests/http/custom-routes.spec.ts` with the following content:

```ts title="integration-tests/http/custom-routes.spec.ts"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      describe("GET /custom", () => {
        it("returns correct message", async () => {
          const response = await api.get(
            `/custom`
          )
  
          expect(response.status).toEqual(200)
          expect(response.data).toHaveProperty("message")
          expect(response.data.message).toEqual("Hello, World!")
        })
      })
    })
  },
})
```

Then, run the test with the following command:

```bash npm2yarn
npm run test:integration
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/debugging-and-testing/testing-tools/integration-tests/api-routes/index.html.md).

### Test Workflow

To create a test for a workflow, create the file `integration-tests/http/workflow.spec.ts` with the following content:

```ts title="integration-tests/http/workflow.spec.ts"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { helloWorldWorkflow } from "../../src/workflows/hello-world"

medusaIntegrationTestRunner({
  testSuite: ({ getContainer }) => {
    describe("Test hello-world workflow", () => {
      it("returns message", async () => {
        const { result } = await helloWorldWorkflow(getContainer())
          .run()

        expect(result).toEqual("Hello, World!")
      })
    })
  },
})
```

Then, run the test with the following command:

```bash npm2yarn
npm run test:integration
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/debugging-and-testing/testing-tools/integration-tests/workflows/index.html.md).

### Test Module's Service

To create a test for a module's service, create the test under the `__tests__` directory of the module.

For example, create the file `src/modules/blog/__tests__/service.spec.ts` with the following content:

```ts title="src/modules/blog/__tests__/service.spec.ts"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { BLOG_MODULE } from ".."
import BlogModuleService from "../service"
import Post from "../models/post"

moduleIntegrationTestRunner<BlogModuleService>({
  moduleName: BLOG_MODULE,
  moduleModels: [Post],
  resolve: "./modules/blog",
  testSuite: ({ service }) => {
    describe("BlogModuleService", () => {
      it("says hello world", () => {
        const message = service.getMessage()

        expect(message).toEqual("Hello, World!")
      })
    })
  },
})
```

Then, run the test with the following command:

```bash npm2yarn
npm run test:modules
```

***

## Commerce Modules

Medusa provides all its commerce features as separate Commerce Modules, such as the Product or Order modules.

Refer to the [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) documentation for concepts and reference of every module's main service.

### Create an Actor Type to Authenticate

To create an actor type that can authenticate to the Medusa application, such as a `manager`:

1. Create the data model in a module:

```ts
import { model } from "@medusajs/framework/utils"

const Manager = model.define("manager", {
  id: model.id().primaryKey(),
  firstName: model.text(),
  lastName: model.text(),
  email: model.text(),
})

export default Manager
```

2. Use the `setAuthAppMetadataStep` as a step in a workflow that creates a manager:

```ts
import { 
  createWorkflow, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  setAuthAppMetadataStep,
} from "@medusajs/medusa/core-flows"
// other imports...

const createManagerWorkflow = createWorkflow(
  "create-manager",
  function (input: CreateManagerWorkflowInput) {
    const manager = createManagerStep({
      manager: input.manager,
    })

    setAuthAppMetadataStep({
      authIdentityId: input.authIdentityId,
      actorType: "manager",
      value: manager.id,
    })

    return new WorkflowResponse(manager)
  }
)
```

3. Use the workflow in an API route that creates a user (manager) of the actor type:

```ts
import type { 
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
import createManagerWorkflow from "../../workflows/create-manager"

type RequestBody = {
  first_name: string
  last_name: string
  email: string
}

export async function POST(
  req: AuthenticatedMedusaRequest<RequestBody>, 
  res: MedusaResponse
) {
  // If `actor_id` is present, the request carries 
  // authentication for an existing manager
  if (req.auth_context.actor_id) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "Request already authenticated as a manager."
    )
  }

  const { result } = await createManagerWorkflow(req.scope)
    .run({
      input: {
        manager: req.body,
        authIdentityId: req.auth_context.auth_identity_id,
      },
    })
  
    res.status(200).json({ manager: result })
}
```

4. Apply the `authenticate` middleware on the new route in `src/api/middlewares.ts`:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  authenticate,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/manager",
      method: "POST",
      middlewares: [
        authenticate("manager", ["session", "bearer"], {
          allowUnregistered: true,
        }),
      ],
    },
    {
      matcher: "/manager/me*",
      middlewares: [
        authenticate("manager", ["session", "bearer"]),
      ],
    },
  ],
})
```

Now, manager users can use the `/manager` API route to register, and all routes starting with `/manager/me` are only accessible by authenticated managers.

Find an elaborate example and learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md).

### Apply Promotion on Cart Items and Shipping

To apply a promotion on a cart's items and shipping methods using the [Cart](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md) and [Promotion](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md) modules:

```ts
import {
  ComputeActionAdjustmentLine,
  ComputeActionItemLine,
  ComputeActionShippingLine,
  AddItemAdjustmentAction,
  AddShippingMethodAdjustment,
  // ...
} from "@medusajs/framework/types"

// retrieve the cart
const cart = await cartModuleService.retrieveCart("cart_123", {
  relations: [
    "items.adjustments",
    "shipping_methods.adjustments",
  ],
})

// retrieve line item adjustments
const lineItemAdjustments: ComputeActionItemLine[] = []
cart.items.forEach((item) => {
  const filteredAdjustments = item.adjustments?.filter(
    (adjustment) => adjustment.code !== undefined
  ) as unknown as ComputeActionAdjustmentLine[]
  if (filteredAdjustments.length) {
    lineItemAdjustments.push({
      ...item,
      adjustments: filteredAdjustments,
    })
  }
})

// retrieve shipping method adjustments
const shippingMethodAdjustments: ComputeActionShippingLine[] =
  []
cart.shipping_methods.forEach((shippingMethod) => {
  const filteredAdjustments =
    shippingMethod.adjustments?.filter(
      (adjustment) => adjustment.code !== undefined
    ) as unknown as ComputeActionAdjustmentLine[]
  if (filteredAdjustments.length) {
    shippingMethodAdjustments.push({
      ...shippingMethod,
      adjustments: filteredAdjustments,
    })
  }
})

// compute actions
const actions = await promotionModuleService.computeActions(
  ["promo_123"],
  {
    items: lineItemAdjustments,
    shipping_methods: shippingMethodAdjustments,
  }
)

// set the adjustments on the line item
await cartModuleService.setLineItemAdjustments(
  cart.id,
  actions.filter(
    (action) => action.action === "addItemAdjustment"
  ) as AddItemAdjustmentAction[]
)

// set the adjustments on the shipping method
await cartModuleService.setShippingMethodAdjustments(
  cart.id,
  actions.filter(
    (action) =>
      action.action === "addShippingMethodAdjustment"
  ) as AddShippingMethodAdjustment[]
)
```

Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/tax-lines/index.html.md).

### Retrieve Tax Lines of a Cart's Items and Shipping

To retrieve the tax lines of a cart's items and shipping methods using the [Cart](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md) and [Tax](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/index.html.md) modules:

```ts
// retrieve the cart
const cart = await cartModuleService.retrieveCart("cart_123", {
  relations: [
    "items.tax_lines",
    "shipping_methods.tax_lines",
    "shipping_address",
  ],
})

// retrieve the tax lines
const taxLines = await taxModuleService.getTaxLines(
  [
    ...(cart.items as TaxableItemDTO[]),
    ...(cart.shipping_methods as TaxableShippingDTO[]),
  ],
  {
    address: {
      ...cart.shipping_address,
      country_code:
        cart.shipping_address.country_code || "us",
    },
  }
)

// set line item tax lines
await cartModuleService.setLineItemTaxLines(
  cart.id,
  taxLines.filter((line) => "line_item_id" in line)
)

// set shipping method tax lines
await cartModuleService.setLineItemTaxLines(
  cart.id,
  taxLines.filter((line) => "shipping_line_id" in line)
)
```

Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/tax-lines/index.html.md)

### Apply Promotion on an Order's Items and Shipping

To apply a promotion on an order's items and shipping methods using the [Order](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) and [Promotion](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md) modules:

```ts
import {
  ComputeActionAdjustmentLine,
  ComputeActionItemLine,
  ComputeActionShippingLine,
  AddItemAdjustmentAction,
  AddShippingMethodAdjustment,
  // ...
} from "@medusajs/framework/types"

// ...

// retrieve the order
const order = await orderModuleService.retrieveOrder("ord_123", {
  relations: [
    "items.item.adjustments",
    "shipping_methods.shipping_method.adjustments",
  ],
})
// retrieve the line item adjustments
const lineItemAdjustments: ComputeActionItemLine[] = []
order.items.forEach((item) => {
  const filteredAdjustments = item.adjustments?.filter(
    (adjustment) => adjustment.code !== undefined
  ) as unknown as ComputeActionAdjustmentLine[]
  if (filteredAdjustments.length) {
    lineItemAdjustments.push({
      ...item,
      ...item.detail,
      adjustments: filteredAdjustments,
    })
  }
})

//retrieve shipping method adjustments
const shippingMethodAdjustments: ComputeActionShippingLine[] =
  []
order.shipping_methods.forEach((shippingMethod) => {
  const filteredAdjustments =
    shippingMethod.adjustments?.filter(
      (adjustment) => adjustment.code !== undefined
    ) as unknown as ComputeActionAdjustmentLine[]
  if (filteredAdjustments.length) {
    shippingMethodAdjustments.push({
      ...shippingMethod,
      adjustments: filteredAdjustments,
    })
  }
})

// compute actions
const actions = await promotionModuleService.computeActions(
  ["promo_123"],
  {
    items: lineItemAdjustments,
    shipping_methods: shippingMethodAdjustments,
    // TODO infer from cart or region
    currency_code: "usd",
  }
)

// set the adjustments on the line items
await orderModuleService.setOrderLineItemAdjustments(
  order.id,
  actions.filter(
    (action) => action.action === "addItemAdjustment"
  ) as AddItemAdjustmentAction[]
)

// set the adjustments on the shipping methods
await orderModuleService.setOrderShippingMethodAdjustments(
  order.id,
  actions.filter(
    (action) =>
      action.action === "addShippingMethodAdjustment"
  ) as AddShippingMethodAdjustment[]
)
```

Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/promotion-adjustments/index.html.md)

### Accept Payment using Module

To accept payment using the Payment Module's main service:

1. Create a payment collection and link it to the cart:

```ts
import { 
  ContainerRegistrationKeys,
  Modules,
} from "@medusajs/framework/utils"

// ...

const paymentCollection =
  await paymentModuleService.createPaymentCollections({
    region_id: "reg_123",
    currency_code: "usd",
    amount: 5000,
  })

// resolve Link
const link = container.resolve(
  ContainerRegistrationKeys.LINK
)

// create a link between the cart and payment collection
link.create({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.PAYMENT]: {
    payment_collection_id: paymentCollection.id,
  },
})
```

2. Create a payment session in the collection:

```ts
const paymentSession =
  await paymentModuleService.createPaymentSession(
    paymentCollection.id,
    {
      provider_id: "stripe",
      currency_code: "usd",
      amount: 5000,
      data: {
        // any necessary data for the
        // payment provider
      },
    }
  )
```

3. Authorize the payment session:

```ts
const payment =
  await paymentModuleService.authorizePaymentSession(
    paymentSession.id,
    {}
  )
```

Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-flow/index.html.md).

### Get Variant's Prices for Region and Currency

To get prices of a product variant for a region and currency using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md):

```ts
import { QueryContext } from "@medusajs/framework/utils"

// ...

const { data: products } = await query.graph({
  entity: "product",
  fields: [
    "*",
    "variants.*",
    "variants.calculated_price.*",
  ],
  filters: {
    id: "prod_123",
  },
  context: {
    variants: {
      calculated_price: QueryContext({
        region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS",
        currency_code: "eur",
      }),
    },
  },
})
```

Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price#retrieve-calculated-price-for-a-context/index.html.md).

### Get All Variant's Prices

To get all prices of a product variant using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md):

```ts
const { data: products } = await query.graph({
  entity: "product",
  fields: [
    "*",
    "variants.*",
    "variants.prices.*",
  ],
  filters: {
    id: [
      "prod_123",
    ],
  },
})
```

Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price/index.html.md).

### Get Variant Prices with Taxes

To get a variant's prices with taxes using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) and the [Tax Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/index.html.md)

```ts
import {
  HttpTypes,
  TaxableItemDTO,
  ItemTaxLineDTO,
} from "@medusajs/framework/types"
import { 
  QueryContext,
  calculateAmountsWithTax,
} from "@medusajs/framework/utils"
// other imports...

// ...
const asTaxItem = (product: HttpTypes.StoreProduct): TaxableItemDTO[] => {
  return product.variants
    ?.map((variant) => {
      if (!variant.calculated_price) {
        return
      }

      return {
        id: variant.id,
        product_id: product.id,
        product_name: product.title,
        product_categories: product.categories?.map((c) => c.name),
        product_category_id: product.categories?.[0]?.id,
        product_sku: variant.sku,
        product_type: product.type,
        product_type_id: product.type_id,
        quantity: 1,
        unit_price: variant.calculated_price.calculated_amount,
        currency_code: variant.calculated_price.currency_code,
      }
    })
    .filter((v) => !!v) as unknown as TaxableItemDTO[]
}

const { data: products } = await query.graph({
  entity: "product",
  fields: [
    "*",
    "variants.*",
    "variants.calculated_price.*",
  ],
  filters: {
    id: "prod_123",
  },
  context: {
    variants: {
      calculated_price: QueryContext({
        region_id: "region_123",
        currency_code: "usd",
      }),
    },
  },
})

const taxLines = (await taxModuleService.getTaxLines(
  products.map(asTaxItem).flat(),
  {
    // example of context properties. You can pass other ones.
    address: {
      country_code,
    },
  }
)) as unknown as ItemTaxLineDTO[]

const taxLinesMap = new Map<string, ItemTaxLineDTO[]>()
taxLines.forEach((taxLine) => {
  const variantId = taxLine.line_item_id
  if (!taxLinesMap.has(variantId)) {
    taxLinesMap.set(variantId, [])
  }

  taxLinesMap.get(variantId)?.push(taxLine)
})

products.forEach((product) => {
  product.variants?.forEach((variant) => {
    if (!variant.calculated_price) {
      return
    }

    const taxLinesForVariant = taxLinesMap.get(variant.id) || []
    const { priceWithTax, priceWithoutTax } = calculateAmountsWithTax({
      taxLines: taxLinesForVariant,
      amount: variant.calculated_price!.calculated_amount!,
      includesTax:
        variant.calculated_price!.is_calculated_price_tax_inclusive!,
    })

    // do something with prices...
  })
})
```

Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price-with-taxes/index.html.md).

### Invite Users

To invite a user using the [User Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/index.html.md):

```ts
const invite = await userModuleService.createInvites({
  email: "user@example.com",
})
```

### Accept User Invite

To accept an invite and create a user using the [User Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/index.html.md):

```ts
const invite =
  await userModuleService.validateInviteToken(inviteToken)

await userModuleService.updateInvites({
  id: invite.id,
  accepted: true,
})

const user = await userModuleService.createUsers({
  email: invite.email,
})
```


# How to Add Custom Authentication in Medusa Admin

In this guide, you'll learn how to add custom authentication provider to the Medusa Admin.

Authenticating into the Medusa Admin with third-party providers is supported from [Medusa v2.12.0](https://github.com/medusajs/medusa/releases/tag/v2.12.0).

## Overview

By default, the Medusa Admin allows users to authenticate using their email and password. Medusa uses the [Emailpass Authentication Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/emailpass/index.html.md) to handle this authentication method.

You can also integrate a custom or third-party authentication provider to allow users registered with that provider to log in to the Medusa Admin. This delegates the authentication process to the third-party provider, which is useful if your organization uses a centralized authentication system.

Ensure the third-party authentication provider allows **only** your organization's users to log in to the Medusa Admin. For example, integrating a social login provider like Google or Facebook without restrictions will allow any user with an account on those platforms to access your Medusa Admin.

### Summary of Custom Medusa Admin Authentication

To authenticate admin users through a third-party provider, you need to:

1. Create a [custom Authentication Module Provider](https://docs.medusajs.com/references/auth/provider/index.html.md) that integrates the third-party provider. For example, you can create a provider that integrates with Okta.
2. Change the authentication type in the Medusa Admin dashboard to use JWT authentication.
3. Add an API route with a workflow that handles creating the admin user after they authenticate through the third-party provider.
4. Add a widget on the login page that provides the option to log in through the third-party provider.

![Overview of the custom authentication process in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1764153489/Medusa%20Resources/admin-third-party-auth_phknrs.jpg)

***

## Step 1: Create a Custom Authentication Module Provider

The first step is to create a custom Authentication Module Provider that integrates with the third-party authentication provider you want to use.

For example, if your organization uses Okta for authentication, you can create an Okta Authentication Provider that uses the Okta SDK to authenticate users.

Refer to the [Create Custom Authentication Module Provider](https://docs.medusajs.com/references/auth/provider/index.html.md) guide to learn how to create a custom Authentication Module Provider.

### Enable Custom Authentication Provider for Admin Users

By default, registered Authentication Module Providers can be used for all [actor types](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-identity-and-actor-types/index.html.md). This means both admin users and customers can authenticate through the provider.

It's recommended to restrict the custom authentication provider to admin users only. To do this, set the `http.authMethodsPerActor` configuration in `medusa-config.ts` to enable the custom authentication provider only for the `user` actor type:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      // ...
      authMethodsPerActor: {
        user: ["emailpass", "custom"],
        customer: ["emailpass"],
      },
    },
    // ...
  },
})
```

Where `emailpass` is the identifier of the Emailpass Authentication Provider, and `custom` is the identifier of your custom Authentication Module Provider.

Refer to the [Authentication Module Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers#configure-allowed-auth-providers-of-actor-types/index.html.md) guide to learn more about configuring allowed authentication providers for actor types.

***

## Step 2: Change Authentication Type in Medusa Admin

Next, you need to change the authentication type in the Medusa Admin dashboard to use JWT authentication.

To do this, set the `ADMIN_AUTH_TYPE` environment variable to `jwt` in your Medusa application's environment variables:

```bash
ADMIN_AUTH_TYPE=jwt
```

Make sure to also create a JS SDK instance in your Medusa Admin customizations with the `auth.type` option set to `jwt`. For example, create the file `src/lib/sdk.ts` with the following content:

```ts title="src/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "jwt",
  },
})
```

Refer to the [JS SDK Authentication guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/auth/overview/index.html.md) to learn more about configuring authentication in the Medusa JS SDK.

***

## Step 3: Create User API Route

Next, you need to create an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) with a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) that handles creating the admin user after they authenticate through the third-party provider.

Note that this API route will allow any user with a valid authentication token to create an admin user. Therefore, ensure that only authorized users can obtain an authentication token from the third-party provider.

### Create User Workflow

First, you need to create the workflow that creates a user and associates it with the authenticated identity.

Create the file `src/workflows/create-user.ts` with the following content:

```ts title="src/workflows/create-user.ts"
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { createUsersWorkflow, setAuthAppMetadataStep } from "@medusajs/medusa/core-flows"

type WorkflowInput = {
  email: string
  auth_identity_id: string
}

export const createUserWorkflow = createWorkflow(
  "create-user",
  (input: WorkflowInput) => {
    const users = createUsersWorkflow.runAsStep({
      input: {
        users: [
          {
            email: input.email,
          },
        ],
      },
    })

    const authUserInput = transform({ input, users }, ({ input, users }) => {
      const createdUser = users[0]

      return {
        authIdentityId: input.auth_identity_id,
        actorType: "user",
        value: createdUser.id,
      }
    })

    setAuthAppMetadataStep(authUserInput)

    return new WorkflowResponse({
      user: users[0],
    })
  }
)
```

The workflow receives an object with the following properties:

- `email`: The email of the user being authenticated. You can modify this based on your third-party provider's identification method.
- `auth_identity_id`: The ID of the auth identity created by the custom Authentication Module Provider. Refer to the [Auth Identities](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-identity-and-actor-types/index.html.md) guide to learn more.

In the workflow, you:

1. Create a user with the provided email using `createUsersWorkflow`.
2. Associate the created user with the authenticated identity using `setAuthAppMetadataStep`.

### Create User API Route

Next, you need to create the API route that calls the workflow to create the user.

Create the file `src/api/custom-auth/admin/users/route.ts` with the following content:

```ts title="src/api/custom-auth/admin/users/route.ts"
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework"
import { z } from "zod"
import { createUserWorkflow } from "../../../../workflows/create-user"

export const CreateUserSchema = z.object({
  email: z.string(),
})

type CreateUserBody = z.infer<typeof CreateUserSchema>

export const POST = async (req: AuthenticatedMedusaRequest<CreateUserBody>, res: MedusaResponse) => {
  const user = await createUserWorkflow(req.scope)
    .run({
      input: {
        email: req.body.email,
        auth_identity_id: req.auth_context!.auth_identity_id!,
      },
    })

  return res.status(200).json({ user })
}
```

You expose a `POST` API route at `/custom-auth/admin/users` that receives the user's email in the request body. It executes `createUserWorkflow` to create the user and returns the created user in the response.

### Apply Authentication Middleware

Finally, you need to apply the authentication [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) to the API route to ensure that only authenticated users can access it.

To do this, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import { authenticate, defineMiddlewares } from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom-auth/admin/users",
      methods: ["POST"],
      middlewares: [
        authenticate("user", "bearer", {
          allowUnregistered: true,
        }),
      ],
    },
  ],
})
```

This middleware applies the `authenticate` middleware to the `/custom-auth/admin/users` `POST` route, allowing only authenticated users to access it. By enabling the `allowUnregistered` option, users who are authenticated through the third-party provider but don't yet have an associated Medusa user can still access the route to create their user.

Refer to the [Protected Routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md) guide to learn more about protecting API routes with the `authentication` middleware.

***

## Step 4: Add a Widget on the Login Page

Finally, you need to add a [widget](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md) to the Medusa Admin login page that provides the option to log in through the third-party provider.

The widget should display a button that authenticates or redirects the user to the third-party provider. The widget should also handle the redirection back to the Medusa Admin after successful authentication.

For example, the widget may look like this:

```tsx title="src/admin/widgets/custom-login.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Button, toast } from "@medusajs/ui"
import { decodeToken } from "react-jwt"
import { useSearchParams, useNavigate } from "react-router-dom"
import { useMutation } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"
import { useEffect } from "react"

// Replace with the identifier of your custom authentication provider
const CUSTOM_AUTH_PROVIDER = "custom"

const CustomLogin = () => {
  // The third-party provider redirects back with query parameters
  // used to validate the authentication
  const [searchParams] = useSearchParams()
  const navigate = useNavigate()
  const { mutateAsync, isPending } = useMutation({
    mutationFn: async () => {
      if (isPending) {
        return
      }
      return await validateCallback()
    },
    onError: (error) => {
      console.error("Custom authentication error:", error)
    },
  })
  
  const sendCallback = async () => {
    try {
      return await sdk.auth.callback(
        "user", 
        CUSTOM_AUTH_PROVIDER, 
        Object.fromEntries(searchParams)
      )
    } catch (error) {
      toast.error("Authentication failed")
      throw error
    }
  }

  // Validate the authentication callback
  const validateCallback = async () => {
    const token = await sendCallback()

    const decodedToken = decodeToken(token) as { actor_id: string, user_metadata: Record<string, unknown> }

    const userExists = decodedToken.actor_id !== ""

    if (!userExists) {
      // Create user
      await sdk.client.fetch("/custom-auth/admin/users", {
        method: "POST",
        body: {
          email: decodedToken.user_metadata?.email as string,
        },
      })

      const newToken = await sdk.auth.refresh()

      if (!newToken) {
        toast.error("Authentication failed")
        return
      }
    }

    // User is authenticated
    navigate("/orders")
  }

  // Handle custom login button click
  const customLogin = async () => {
    const result = await sdk.auth.login("user", CUSTOM_AUTH_PROVIDER, {})

    if (typeof result === "object" && result.location) {
      // Redirect to custom provider for authentication
      window.location.href = result.location
      return
    }
    
    if (typeof result !== "string") {
      // Result failed, show an error
      toast.error("Authentication failed")
      return
    }

    navigate("/app")
  }

  // Handle the redirection back from the third-party provider
  useEffect(() => {
    // Check for provider-specific query parameters
    if (searchParams.get("code")) {
      mutateAsync()
    }
  }, [searchParams, mutateAsync])

  return (
    <>
      <hr className="bg-ui-border-base my-4" />
      <Button 
        variant="secondary" 
        onClick={customLogin} 
        className="w-full"
      >
        Login with Custom Provider
      </Button>
    </>
  )
}

export const config = defineWidgetConfig({
  zone: "login.after",
})

export default CustomLogin
```

You inject the widget into the `login.after` zone. This displays it below the default email and password login form.

You can't remove the default email and password login form from the Medusa Admin. You can only add additional login options through widgets.

In the widget, you:

1. Display a button that initiates login through the third-party provider using the `sdk.auth.login` method.
   - If the login method returns a `location` property, redirect the user to that location to authenticate through the third-party provider.
   - Otherwise, if the login method returns a token, the user is authenticated and you navigate to the Orders page of the admin dashboard.
2. Add a `useEffect` hook that checks for provider-specific query parameters in the URL, such as `code`, to handle the redirection back from the third-party provider after successful authentication.
3. Upon redirection back, you:
   - Call the `sdk.auth.callback` method to validate the authentication with the third-party provider.
   - If the user doesn't exist in Medusa, call the [custom API route you created](#step-3-create-user-api-route) to create the user, then refresh the authentication token.
   - Finally, if authentication is successful, navigate to the Orders page of the admin dashboard.

***

## Test Custom Authentication in Medusa Admin

To test the custom authentication in the Medusa Admin, run the following command to start your Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin at `http://localhost:9000/app`, which will redirect you to the login page.

On the login page, you should see the option to log in through the third-party provider you integrated. Click the button to log in through the provider and follow the authentication flow.

If authentication is successful, you'll have access to the Medusa Admin dashboard. Otherwise, you can troubleshoot the issue by checking your Medusa application's logs or the browser's console for errors.


# How-to & Tutorials

In this section of the documentation, you'll find how-to guides and tutorials that will help you customize the Medusa server and admin. These guides are useful after you've learned Medusa's main concepts in the [Get Started](https://docs.medusajs.com/docs/learn/index.html.md) section of the documentation.

You can follow these guides to learn how to customize the Medusa server and admin to fit your business requirements. This section of the documentation also includes deployment guides to help you deploy your Medusa server and admin to different platforms.

## Example Snippets

For a quick access to code snippets of the different concepts you learned about, such as API routes and workflows, refer to the [Examples Snippets](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/examples/index.html.md) documentation.

***

***

## Deployment Guides

Deployment guides are a collection of guides that help you deploy your Medusa server and admin to different platforms. Learn more in the [Deployment Overview](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/deployment/index.html.md) documentation.


# Send Abandoned Cart Notifications in Medusa

In this tutorial, you will learn how to send notifications to customers who have abandoned their carts.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md), which are available out-of-the-box. These features include cart-management capabilities.

Medusa's [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) allows you to send notifications to users or customers, such as password reset emails, order confirmation SMS, or other types of notifications.

In this tutorial, you will use the Notification Module to send an email to customers who have abandoned their carts. The email will contain a link to recover the customer's cart, encouraging them to complete their purchase. You will use SendGrid to send the emails, but you can also use other email providers.

## Summary

By following this tutorial, you will:

- Install and set up Medusa.
- Create the logic to send an email to customers who have abandoned their carts.
- Run the above logic once a day.
- Add a route to the storefront to recover the cart.

![Diagram illustrating the flow of the abandoned-cart functionalities](https://res.cloudinary.com/dza7lstvk/image/upload/v1742460588/Medusa%20Resources/abandoned-cart-summary_fcf2tn.jpg)

[View on Github](https://github.com/medusajs/examples/tree/main/abandoned-cart): Find the full code for this tutorial.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You will first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Set up SendGrid

### Prerequisites

- [SendGrid account](https://sendgrid.com)
- [Verified Sender Identity](https://mc.sendgrid.com/senders)
- [SendGrid API Key](https://app.sendgrid.com/settings/api_keys)

Medusa's Notification Module provides the general functionality to send notifications, but the sending logic is implemented in a module provider. This allows you to integrate the email provider of your choice.

To send the cart-abandonment emails, you will use SendGrid. Medusa provides a [SendGrid Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md) that you can use to send emails.

Alternatively, you can use [other Notification Module Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification#what-is-a-notification-module-provider/index.html.md) or [create a custom provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md).

To set up SendGrid, add the SendGrid Notification Module Provider to `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/notification-sendgrid",
            id: "sendgrid",
            options: {
              channels: ["email"],
              api_key: process.env.SENDGRID_API_KEY,
              from: process.env.SENDGRID_FROM,
            },
          },
        ],
      },
    },
  ],
})
```

In the `modules` configuration, you pass the Notification Provider and add SendGrid as a provider. You also pass to the SendGrid Module Provider the following options:

- `channels`: The channels that the provider supports. In this case, it is only email.
- `api_key`: Your SendGrid API key.
- `from`: The email address that the emails will be sent from.

Then, set the SendGrid API key and "from" email as environment variables, such as in the `.env` file at the root of your project:

```plain
SENDGRID_API_KEY=your-sendgrid-api-key
SENDGRID_FROM=test@gmail.com
```

You can now use SendGrid to send emails in Medusa.

***

## Step 3: Send Abandoned Cart Notification Flow

You will now implement the sending logic for the abandoned cart notifications.

To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it is a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in a scheduled job.

In this step, you will create the workflow that sends the abandoned cart notifications. Later, you will learn how to execute it once a day.

The workflow will receive the list of abandoned carts as an input. The workflow has the following steps:

- [sendAbandonedNotificationsStep](#sendAbandonedNotificationsStep): Send the abandoned cart notifications.
- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to store the last notification date.

Medusa provides the second step in its `@medusajs/medusa/core-flows` package. So, you only need to implement the first one.

### sendAbandonedNotificationsStep

The first step of the workflow sends a notification to the owners of the abandoned carts that are passed as an input.

To implement the step, create the file `src/workflows/steps/send-abandoned-notifications.ts` with the following content:

```ts title="src/workflows/steps/send-abandoned-notifications.ts"
import { 
  createStep,
  StepResponse, 
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
import { CartDTO, CustomerDTO } from "@medusajs/framework/types"

type SendAbandonedNotificationsStepInput = {
  carts: (CartDTO & {
    customer: CustomerDTO
  })[]
}

export const sendAbandonedNotificationsStep = createStep(
  "send-abandoned-notifications",
  async (input: SendAbandonedNotificationsStepInput, { container }) => {
    const notificationModuleService = container.resolve(
      Modules.NOTIFICATION
    )

    const notificationData = input.carts.map((cart) => ({
      to: cart.email!,
      channel: "email", 
      template: process.env.ABANDONED_CART_TEMPLATE_ID || "",
      data: {
        customer: {
          first_name: cart.customer?.first_name || cart.shipping_address?.first_name,
          last_name: cart.customer?.last_name || cart.shipping_address?.last_name,
        },
        cart_id: cart.id,
        items: cart.items?.map((item) => ({
          product_title: item.title,
          quantity: item.quantity,
          unit_price: item.unit_price,
          thumbnail: item.thumbnail,
        })),
      },
    }))

    const notifications = await notificationModuleService.createNotifications(
      notificationData
    )

    return new StepResponse({
      notifications,
    })
  }
)
```

You create a step with `createStep` from the Workflows SDK. It accepts two parameters:

1. The step's unique name, which is `create-review`.
2. An async function that receives two parameters:
   - The step's input, which is in this case an object with the review's properties.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.

In the step function, you first resolve the Notification Module's service, which has methods to manage notifications. Then, you prepare the data of each notification, and create the notifications with the `createNotifications` method.

Notice that each notification is an object with the following properties:

- `to`: The email address of the customer.
- `channel`: The channel that the notification will be sent through. The Notification Module uses the provider registered for the channel.
- `template`: The ID or name of the email template in the third-party provider. Make sure to set it as an environment variable once you have it.
- `data`: The data to pass to the template to render the email's dynamic content.

Based on the dynamic template you create in SendGrid or another provider, you can pass different data in the `data` object.

A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts the step's output as a parameter, which is the created notifications.

### Create Workflow

You can now create the workflow that uses the step you just created to send the abandoned cart notifications.

Create the file `src/workflows/send-abandoned-carts.ts` with the following content:

```ts title="src/workflows/send-abandoned-carts.ts"
import {
  createWorkflow,
  WorkflowResponse,
  transform,
} from "@medusajs/framework/workflows-sdk"
import { 
  sendAbandonedNotificationsStep,
} from "./steps/send-abandoned-notifications"
import { updateCartsStep } from "@medusajs/medusa/core-flows"
import { CartDTO } from "@medusajs/framework/types"
import { CustomerDTO } from "@medusajs/framework/types"

export type SendAbandonedCartsWorkflowInput = {
  carts: (CartDTO & {
    customer: CustomerDTO
  })[]
}

export const sendAbandonedCartsWorkflow = createWorkflow(
  "send-abandoned-carts",
  function (input: SendAbandonedCartsWorkflowInput) {
    sendAbandonedNotificationsStep(input)

    const updateCartsData = transform(
      input,
      (data) => {
        return data.carts.map((cart) => ({
          id: cart.id,
          metadata: {
            ...cart.metadata,
            abandoned_notification: new Date().toISOString(),
          },
        }))
      }
    )

    const updatedCarts = updateCartsStep(updateCartsData)

    return new WorkflowResponse(updatedCarts)
  }
)
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is an arra of carts.

In the workflow's constructor function, you:

- Use the `sendAbandonedNotificationsStep` to send the notifications to the carts' customers.
- Use the `updateCartsStep` from Medusa's core flows to update the carts' metadata with the last notification date.

Notice that you use the `transform` function to prepare the `updateCartsStep`'s input. Medusa does not support direct data manipulation in a workflow's constructor function. You can learn more about it in the [Data Manipulation in Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md).

Your workflow is now ready for use. You will learn how to execute it in the next section.

### Setup Email Template

Before you can test the workflow, you need to set up an email template in SendGrid. The template should contain the dynamic content that you pass in the workflow's step.

To create an email template in SendGrid:

- Go to [Dynamic Templates](https://mc.sendgrid.com/dynamic-templates) in the SendGrid dashboard.
- Click on the "Create Dynamic Template" button.

![Button is at the top right of the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1742457153/Medusa%20Resources/Screenshot_2025-03-20_at_9.51.38_AM_g5nk80.png)

- In the side window that opens, enter a name for the template, then click on the Create button.
- The template will be added to the middle of the page. When you click on it, a new section will show with an "Add Version" button. Click on it.

![The template is a collapsible in the middle of the page,with the "Add Version" button shown in the middle](https://res.cloudinary.com/dza7lstvk/image/upload/v1742458096/Medusa%20Resources/Screenshot_2025-03-20_at_10.07.54_AM_y2dys7.png)

In the form that opens, you can either choose to start with a blank template or from an existing design. You can then use the drag-and-drop or code editor to design the email template.

You can also use the following template as an example:

```html title="Abandoned Cart Email Template"
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Complete Your Purchase</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f8f9fa;
            margin: 0;
            padding: 20px;
        }
        .container {
            max-width: 600px;
            background: #ffffff;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
            text-align: center;
        }
        .header {
            font-size: 26px;
            font-weight: bold;
            color: #333;
            margin-bottom: 20px;
        }
        .message {
            font-size: 16px;
            color: #555;
            margin-bottom: 20px;
        }
        .item {
            display: flex;
            align-items: center;
            background: #f9f9f9;
            padding: 10px;
            border-radius: 8px;
            margin-bottom: 15px;
        }
        .item img {
            width: 80px;
            height: auto;
            margin-right: 15px;
            border-radius: 5px;
        }
        .item-details {
            text-align: left;
            flex-grow: 1;
        }
        .item-details strong {
            font-size: 18px;
            color: #333;
        }
        .item-details p {
            font-size: 14px;
            color: #777;
            margin: 5px 0;
        }
        .button {
            display: inline-block;
            background-color: #007bff;
            color: #ffffff;
            text-decoration: none;
            font-size: 18px;
            padding: 12px 20px;
            border-radius: 5px;
            margin-top: 20px;
            transition: background 0.3s ease;
        }
        .button:hover {
            background-color: #0056b3;
        }
        .footer {
            font-size: 12px;
            color: #888;
            margin-top: 20px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">Hi {{customer.first_name}}, your cart is waiting! 🛍️</div>
        <p class="message">You left some great items in your cart. Complete your purchase before they're gone!</p>
        
        {{#each items}}
        <div class="item">
            <img src="{{thumbnail}}" alt="{{product_title}}">
            <div class="item-details">
                <strong>{{product_title}}</strong>
                <p>{{subtitle}}</p>
                <p>Quantity: <strong>{{quantity}}</strong></p>
                <p>Price: <strong>$ {{unit_price}}</strong></p>
            </div>
        </div>
        {{/each}}
        
        <a href="https://yourstore.com/cart/recover/{{cart_id}}" class="button">Return to Cart & Checkout</a>
        <p class="footer">Need help? <a href="mailto:support@yourstore.com">Contact us</a></p>
    </div>
</body>
</html>
```

This template will show each item's image, title, quantity, and price in the cart. It will also show a button to return to the cart and checkout.

You can replace `https://yourstore.com` with your storefront's URL. You'll later implement the `/cart/recover/:cart_id` route in the storefront to recover the cart.

Once you are done, copy the template ID from SendGrid and set it as an environment variable in your Medusa project:

```plain
ABANDONED_CART_TEMPLATE_ID=your-sendgrid-template-id
```

***

## Step 4: Schedule Cart Abandonment Notifications

The next step is to automate sending the abandoned cart notifications. You need a task that runs once a day to find the carts that have been abandoned for a certain period and send the notifications to the customers.

To run a task at a scheduled interval, you can use a [scheduled job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md). A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime.

You can create a scheduled job in a TypeScript or JavaScript file under the `src/jobs` directory. So, to create the scheduled job that sends the abandoned cart notifications, create the file `src/jobs/send-abandoned-cart-notification.ts` with the following content:

```ts title="src/jobs/send-abandoned-cart-notification.ts"
import { MedusaContainer } from "@medusajs/framework/types"
import { 
  sendAbandonedCartsWorkflow, 
  SendAbandonedCartsWorkflowInput,
} from "../workflows/send-abandoned-carts"

export default async function abandonedCartJob(
  container: MedusaContainer
) {
  const logger = container.resolve("logger")
  const query = container.resolve("query")

  const oneDayAgo = new Date()
  oneDayAgo.setDate(oneDayAgo.getDate() - 1)
  const limit = 100
  const offset = 0
  const totalCount = 0
  const abandonedCartsCount = 0

  do {
    // TODO retrieve paginated abandoned carts
  } while (offset < totalCount)

  logger.info(`Sent ${abandonedCartsCount} abandoned cart notifications`)
}

export const config = {
  name: "abandoned-cart-notification",
  schedule: "0 0 * * *", // Run at midnight every day
}
```

In a scheduled job's file, you must export:

1. An asynchronous function that holds the job's logic. The function receives the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) as a parameter.
2. A `config` object that specifies the job's name and schedule. The schedule is a [cron expression](https://crontab.guru/) that defines the interval at which the job runs.

In the scheduled job function, so far you resolve the [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) to log messages, and [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve data across modules.

You also define a `oneDayAgo` date, which is the date that you will use as the condition of an abandoned cart. In addition, you define variables to paginate the carts.

Next, you will retrieve the abandoned carts using Query. Replace the `TODO` with the following:

```ts title="src/jobs/send-abandoned-cart-notification.ts"
const { 
  data: abandonedCarts, 
  metadata,
} = await query.graph({
  entity: "cart",
  fields: [
    "id",
    "email",
    "items.*",
    "metadata",
    "customer.*",
  ],
  filters: {
    updated_at: {
      $lt: oneDayAgo,
    },
    email: {
      $ne: null,
    },
    completed_at: null,
  },
  pagination: {
    skip: offset,
    take: limit,
  },
})

totalCount = metadata?.count ?? 0
const cartsWithItems = abandonedCarts.filter((cart) => 
  cart.items?.length > 0 && !cart.metadata?.abandoned_notification
)

try {
  await sendAbandonedCartsWorkflow(container).run({
    input: {
      carts: cartsWithItems,
    } as unknown as SendAbandonedCartsWorkflowInput,
  })
  abandonedCartsCount += cartsWithItems.length

} catch (error) {
  logger.error(
    `Failed to send abandoned cart notification: ${error.message}`
  )
}

offset += limit
```

In the do-while loop, you use Query to retrieve carts matching the following criteria:

- The cart was last updated more than a day ago.
- The cart has an email address.
- The cart has not been completed.

You also filter the retrieved carts to only include carts with items and customers that have not received an abandoned cart notification.

Finally, you execute the `sendAbandonedCartsWorkflow` passing it the abandoned carts as an input. You will execute the workflow for each paginated batch of carts.

### Test it Out

To test out the scheduled job and workflow, it is recommended to change the `oneDayAgo` date to a minute before now for easy testing:

```ts title="src/jobs/send-abandoned-cart-notification.ts"
oneDayAgo.setMinutes(oneDayAgo.getMinutes() - 1) // For testing
```

And to change the job's schedule in `config` to run every minute:

```ts title="src/jobs/send-abandoned-cart-notification.ts"
export const config = {
  // ...
  schedule: "* * * * *", // Run every minute for testing
}
```

Finally, start the Medusa application with the following command:

```bash npm2yarn
npm run dev
```

And in the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md)'s directory (that you installed in the first step), start the storefront with the following command:

```bash npm2yarn
npm run dev
```

Open the storefront at `localhost:8000`. You can either:

- Create an account and add items to the cart, then leave the cart for a minute.
- Add an item to the cart as a guest. Then, start the checkout process, but only enter the shipping and email addresses, and leave the cart for a minute.

Afterwards, wait for the job to execute. Once it is executed, you will see the following message in the terminal:

```bash
info:    Sent 1 abandoned cart notifications
```

Once you're done testing, make sure to revert the changes to the `oneDayAgo` date and the job's schedule.

***

## Step 5: Recover Cart in Storefront

In the storefront, you need to add a route that recovers the cart when the customer clicks on the link in the email. The route should receive the cart ID, set the cart ID in the cookie, and redirect the customer to the cart page.

To implement the route, in the Next.js Starter Storefront create the file `src/app/[countryCode]/(main)/cart/recover/[id]/route.tsx` with the following content:

```tsx title="src/app/[countryCode]/(main)/cart/recover/[id]/route.tsx" badgeLabel="Storefront" badgeColor="blue"
import { NextRequest } from "next/server"
import { retrieveCart } from "../../../../../../lib/data/cart"
import { setCartId } from "../../../../../../lib/data/cookies"
import { notFound, redirect } from "next/navigation"
type Params = Promise<{
  id: string
}>

export async function GET(req: NextRequest, { params }: { params: Params }) {
  const { id } = await params
  const cart = await retrieveCart(id)

  if (!cart) {
    return notFound()
  }

  setCartId(id)

  const countryCode = cart.shipping_address?.country_code || 
    cart.region?.countries?.[0]?.iso_2

  redirect(
    `/${countryCode ? `${countryCode}/` : ""}cart`
  )
}
```

You add a `GET` route handler that receives the cart ID as a path parameter. In the route handler, you:

- Try to retrieve the cart from the Medusa application. The `retrieveCart` function is already available in the Next.js Starter Storefront. If the cart is not found, you return a 404 response.
- Set the cart ID in a cookie using the `setCartId` function. This is also a function that is already available in the storefront.
- Redirect the customer to the cart page. You set the country code in the URL based on the cart's shipping address or region.

### Test it Out

To test it out, start the Medusa application:

```bash npm2yarn
npm run dev
```

And in the Next.js Starter Storefront's directory, start the storefront:

```bash npm2yarn
npm run dev
```

Then, either open the link in an abandoned cart email or navigate to `localhost:8000/cart/recover/:cart_id` in your browser. You will be redirected to the cart page with the recovered cart.

![Cart page in the storefront](https://res.cloudinary.com/dza7lstvk/image/upload/v1742459552/Medusa%20Resources/Screenshot_2025-03-20_at_10.32.17_AM_frmbup.png)

***

## Next Steps

You have now implemented the logic to send abandoned cart notifications in Medusa. You can implement other customizations with Medusa, such as:

- [Implement Product Reviews](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/how-to-tutorials/tutorials/product-reviews/index.html.md).
- [Implement Wishlist](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/plugins/guides/wishlist/index.html.md).
- [Allow Custom-Item Pricing](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/examples/guides/custom-item-price/index.html.md).

If you are new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you will get a more in-depth learning of all the concepts you have used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Implement Agentic Commerce (ChatGPT Instant Checkout) Specifications

In this tutorial, you'll learn how to implement [Agentic Commerce](https://developers.openai.com/commerce) specifications in Medusa that allow you to sell through ChatGPT.

When you install a Medusa application, you get a fully-fledged commerce platform with the Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md), which are available out-of-the-box.

The [Agentic Commerce Protocol](https://developers.openai.com/commerce) supports instant checkout experiences within AI agents. By implementing Agentic Commerce specifications in your Medusa application, customers can purchase products through ChatGPT and other AI agents.

Instant Checkout in ChatGPT is currently available in select regions and for select businesses. The implementation in this guide is based on OpenAI's [Agentic Commerce documentation](https://developers.openai.com/commerce) and may require some adjustments when you apply for [Instant Checkout](https://chatgpt.com/merchants).

## Summary

By following this tutorial, you will learn how to:

- Build a product feed matching the [Agentic Commerce specifications](https://developers.openai.com/commerce/specs/feed).
- Create [Agentic Checkout APIs](https://developers.openai.com/commerce/specs/checkout) that handle checkout requests from AI agents.
- Send webhook events to AI agents matching the [Agentic Commerce specifications](https://developers.openai.com/commerce/specs/webhooks).

By the end of this tutorial, you'll have all necessary resources to apply for [Instant Checkout](https://chatgpt.com/merchants) and start selling in ChatGPT. You can also sell through other AI agents that support the Agentic Commerce Protocol.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Diagram showing the Agentic Commerce integration between user, ChatGPT, and Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1759333742/Medusa%20Resources/agentic-commerce_jqr5wn.jpg)

- [Full Code](https://github.com/medusajs/examples/tree/main/agentic-commerce): Find the full code for this guide in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1759332538/OpenApi/agentic-commerce-openapi_hvsioq.yaml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Begin by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. You can optionally choose to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) as well.

After that, the installation process will begin. This will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Then, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Agentic Commerce Module

To integrate third-party services into Medusa, you create a custom module. A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

In this step, you'll create a module that integrates with an AI agent through the Agentic Commerce Protocol. This module is useful to send the product feed and webhook events to the AI agent.

Refer to the [Modules documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to learn more.

### a. Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/agentic-commerce`.

### b. Create Module Service

You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database (useful if your module defines database tables) or connect to third-party services.

In this section, you'll create the Agentic Commerce Module's service and the methods necessary to interact with the Agentic Commerce Protocol.

To create the service, create the file `src/modules/agentic-commerce/service.ts` with the following content:

```ts title="src/modules/agentic-commerce/service.ts"
type ModuleOptions = {
  // TODO add module options like API key, etc.
  signatureKey: string
}

export default class AgenticCommerceService {
  options: ModuleOptions
  constructor({}, options: ModuleOptions) {
    this.options = options
    // TODO initialize client
  }
}
```

The service's constructor receives two parameters:

- The [Module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) that allows you to resolve Framework tools.
- The module's options that you'll later pass when registering the module in the Medusa application. You can add more options based on your integration.

If you're connecting to AI agents through an SDK or API client, you can initialize it in the constructor.

#### sendProductFeed Method

Next, you'll add the `sendProductFeed` method to the service. This method sends the [product feed](https://developers.openai.com/commerce/specs/feed) to AI agents, allowing them to search and display your products.

Add the following method to the `AgenticCommerceService` class:

```ts title="src/modules/agentic-commerce/service.ts"
export default class AgenticCommerceService {
  // ...
  async sendProductFeed(productFeed: string) {
    // TODO send product feed
    console.log(`Synced product feed ${productFeed}`)
  }
}
```

OpenAI hasn't publicly published the endpoint for sending product feeds. So, in this method, you'll just log the product feed URL to the console.

When you apply for [Instant Checkout](https://chatgpt.com/merchants), you'll get access to the endpoint and can implement the logic to send the product feed in this method.

You'll implement the logic to create product feeds later.

#### verifySignature Method

Next, you'll add the `verifySignature` method to the service. This method verifies signatures of requests sent by AI agents to Agentic Checkout APIs that you'll create later.

Add the following import at the top of the `src/modules/agentic-commerce/service.ts` file:

```ts title="src/modules/agentic-commerce/service.ts"
import crypto from "crypto"
```

Then, add the following method to the `AgenticCommerceService` class:

```ts title="src/modules/agentic-commerce/service.ts"
export default class AgenticCommerceService {
  // ...
  async verifySignature({
    signature,
    payload,
  }: {
    // base64 encoded signature
    signature: string
    payload: any
  }) {
    try {
      // Decode the base64 signature
      const receivedSignature = Buffer.from(signature, "base64")
      
      // Create HMAC-SHA256 signature using your signing key
      const expectedSignature = crypto
        .createHmac("sha256", this.options.signatureKey)
        .update(JSON.stringify(payload), "utf8")
        .digest()
      
      // Compare signatures using constant-time comparison to prevent timing attacks
      return crypto.timingSafeEqual(receivedSignature, expectedSignature)
    } catch (error) {
      console.error("Signature verification failed:", error)
      return false
    }
  }
}
```

This method receives request signatures and payloads, then verifies signatures using HMAC-SHA256 with the module's `signatureKey` option.

When you apply for [Instant Checkout](https://chatgpt.com/merchants), you'll receive a signature key for verifying request signatures. You can set this key in the module's options.

#### getSignature Method

Next, you'll add the `getSignature` method to the service. This method generates signatures for use in request headers when sending webhook events to AI agents.

Add the following method to the `AgenticCommerceService` class:

```ts title="src/modules/agentic-commerce/service.ts"
export default class AgenticCommerceService {
  // ...
  async getSignature(data: any) {
    return Buffer.from(crypto.createHmac("sha256", this.options.signatureKey)
      .update(JSON.stringify(data), "utf8").digest()).toString("base64")
  }
}
```

This method receives webhook event data and generates signatures using HMAC-SHA256 with the module's `signatureKey` option.

#### sendWebhookEvent Method

Finally, you'll add the `sendWebhookEvent` method to the service. This method sends webhook events to AI agents.

First, add the following type at the top of the `src/modules/agentic-commerce/service.ts` file:

```ts title="src/modules/agentic-commerce/service.ts"
export type AgenticCommerceWebhookEvent = {
  type: "order.created" | "order.updated"
  data: {
    type: "order"
    checkout_session_id: string
    permalink_url: string
    status: "created" | "manual_review" | "confirmed" | "canceled" | "shipping" | "fulfilled"
    refunds: {
      type: "store_credit" | "original_payment"
      amount: number
    }[]
  }
}
```

This type defines the structure of webhook events that you can send to AI agents based on [Agentic Commerce specifications](https://developers.openai.com/commerce/specs/webhooks).

Then, add the following method to the `AgenticCommerceService` class:

```ts title="src/modules/agentic-commerce/service.ts"
export default class AgenticCommerceService {
  // ...
  async sendWebhookEvent({
    type,
    data,
  }: AgenticCommerceWebhookEvent) {
    // Create signature
    const signature = this.getSignature(data)
    // TODO send order webhook event
    console.log(`Sent order webhook event ${type} with signature ${signature} and data ${JSON.stringify(data)}`)
  }
}
```

This method receives webhook event types and data, generates signatures using the `getSignature` method, and logs events to the console.

When you apply for [Instant Checkout](https://chatgpt.com/merchants), you'll get access to endpoints for sending webhook events and can implement the logic in this method.

### c. Export Module Definition

The final piece of a module is its definition, which you export in an `index.ts` file at the root directory. This definition tells Medusa the module name and its service.

So, create the file `src/modules/agentic-commerce/index.ts` with the following content:

```ts title="src/modules/agentic-commerce/index.ts"
import AgenticCommerceService from "./service"
import { Module } from "@medusajs/framework/utils"

export const AGENTIC_COMMERCE_MODULE = "agenticCommerce"

export default Module(AGENTIC_COMMERCE_MODULE, {
  service: AgenticCommerceService,
})
```

You use the `Module` function from the Modules SDK to create module definitions. It accepts two parameters:

1. The module name, which is `agenticCommerce`.
2. An object with a required `service` property indicating the module's service.

You also export the module name as `AGENTIC_COMMERCE_MODULE` for later reference.

### d. Add Module to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/agentic-commerce",
      options: {
        signatureKey: process.env.AGENTIC_COMMERCE_SIGNATURE_KEY || "supersecret",
      },
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

You also pass an `options` property with module options, including the signature key. Once you receive a signature key from OpenAI, you can set it in the `AGENTIC_COMMERCE_SIGNATURE_KEY` environment variable.

Your module is now ready for use. You'll build workflows around it in the following steps.

***

## Step 3: Send Product Feed

In this step, you'll create logic to generate and send product feeds matching [Agentic Commerce specifications](https://developers.openai.com/commerce/specs/feed). These feeds provide AI agents with product information, allowing them to search and display products to customers. Customers can then purchase products through AI agents.

You'll implement:

- A [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) that generates and sends the product feed.
- A [scheduled job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md) that executes the workflow every fifteen minutes. This is the maximum frequency OpenAI allows for product feed updates.

### a. Send Product Feed Workflow

A workflow is a series of actions called steps that complete a task. You construct workflows like functions, but they're special functions that allow you to track execution progress, define rollback logic, and configure advanced features.

Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

The workflow for sending product feeds will have the following steps:

- [getProductFeedItemsStep](#getProductFeedItemsStep): Get items to include in the product feed
- [buildProductFeedXmlStep](#buildProductFeedXmlStep): Generate product feed XML
- [sendProductFeedStep](#sendProductFeedStep): Send product feed to AI agent

#### getProductFeedItemsStep

The `getProductFeedItemsStep` step retrieves product variants to include in the product feed.

To create the step, create the file `src/workflows/steps/get-product-feed-items.ts` with the following content:

```ts title="src/workflows/steps/get-product-feed-items.ts" highlights={getProductFeedItemsStepHighlights1}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { getVariantAvailability, QueryContext } from "@medusajs/framework/utils"
import { CalculatedPriceSet, ShippingOptionDTO } from "@medusajs/framework/types"

export type FeedItem = {
  id: string
  title: string
  description: string
  link: string
  image_link?: string
  additional_image_link?: string
  availability: string
  inventory_quantity: number
  price: string
  sale_price?: string
  item_group_id: string
  item_group_title: string
  gtin?: string
  condition?: string
  brand?: string
  product_category?: string
  material?: string
  weight?: string
  color?: string
  size?: string
  seller_name: string
  seller_url: string
  seller_privacy_policy: string
  seller_tos: string
  return_policy: string
  return_window?: number
}

type StepInput = {
  currency_code: string
  country_code: string
}

const formatPrice = (price: number, currency_code: string) => {
  return `${new Intl.NumberFormat("en-US", {
    currency: currency_code,
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
  }).format(price)} ${currency_code.toUpperCase()}`
}

export const getProductFeedItemsStep = createStep(
  "get-product-feed-items", 
  async (input: StepInput, { container }) => {
    // TODO implement step
  }
)
```

You define the `FeedItem` type that matches the structure of an item in the product feed. You also define the `StepInput` type that includes the input parameters for the step, which are the currency and country codes.

Then, you define the `formatPrice` utility function that formats a price in a given currency. This is the format required by the Agentic Commerce specifications.

Finally, you create a step with `createStep` from the Workflows SDK. It accepts two parameters:

1. The step's unique name, which is `get-product-feed-items`.
2. An async function that receives two parameters:
   - The step's input.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.

Next, you'll implement the step's logic. Replace the step implementation with the following:

```ts title="src/workflows/steps/get-product-feed-items.ts"
export const getProductFeedItemsStep = createStep(
  "get-product-feed-items", 
  async (input: StepInput, { container }) => {
  const feedItems: FeedItem[] = []
  const query = container.resolve("query")
  const configModule = container.resolve("configModule")
  const storefrontUrl = configModule.admin.storefrontUrl || 
    process.env.STOREFRONT_URL

  const limit = 100
  let offset = 0
  let count = 0
  const countryCode = input.country_code.toLowerCase()
  const currencyCode = input.currency_code.toLowerCase()

  do {
    const {
      data: products,
      metadata,
    } = await query.graph({
      entity: "product",
      fields: [
        "id",
        "title",
        "description",
        "handle",
        "thumbnail",
        "images.*",
        "status",
        "variants.*",
        "variants.calculated_price.*",
        "sales_channels.*",
        "sales_channels.stock_locations.*",
        "sales_channels.stock_locations.address.*",
        "categories.*",
      ],
      filters: {
        status: "published",
      },
      context: {
        variants: {
          calculated_price: QueryContext({
            currency_code: currencyCode,
          }),
        },
      },
      pagination: {
        take: limit,
        skip: offset,
      },
    })
    
    count = metadata?.count ?? 0
    offset += limit

    for (const product of products) {
      if (!product.variants.length) {continue}
      const salesChannel = product.sales_channels?.find((channel) => {
        return channel?.stock_locations?.some((location) => {
          return location?.address?.country_code.toLowerCase() === countryCode
        })
      })

      const availability = salesChannel?.id ? await getVariantAvailability(query, {
        variant_ids: product.variants.map((variant) => variant.id),
        sales_channel_id: salesChannel?.id,
      }) : undefined

      const categories = product.categories?.map((cat) => cat?.name)
      .filter((name): name is string => !!name).join(">")

      for (const variant of product.variants) {
        // @ts-ignore
        const calculatedPrice = variant.calculated_price as CalculatedPriceSet
        const hasOriginalPrice = 
          calculatedPrice?.original_amount !== calculatedPrice?.calculated_amount
        const originalPrice = hasOriginalPrice ? 
          calculatedPrice.original_amount : calculatedPrice.calculated_amount
        const salePrice = hasOriginalPrice ? 
          calculatedPrice.calculated_amount : undefined
        const stockStatus = !variant.manage_inventory ? "in stock" : 
          !availability?.[variant.id]?.availability ? "out of stock" : "in stock"
        const inventoryQuantity = !variant.manage_inventory ? 
          100000 : availability?.[variant.id]?.availability || 0
        const color = variant.options?.find(
          (o) => o.option?.title.toLowerCase() === "color"
        )?.value
        const size = variant.options?.find(
          (o) => o.option?.title.toLowerCase() === "size"
        )?.value

        feedItems.push({
          id: variant.id,
          title: product.title,
          description: product.description ?? "",
          link: `${storefrontUrl || ""}/${input.country_code}/${product.handle}`,
          image_link: product.thumbnail ?? "",
          additional_image_link: product.images?.map(
            (image) => image.url
          )?.join(","),
          availability: stockStatus,
          inventory_quantity: inventoryQuantity,
          price: formatPrice(originalPrice as number, currencyCode),
          sale_price: salePrice ? 
            formatPrice(salePrice as number, currencyCode) : undefined,
          item_group_id: product.id,
          item_group_title: product.title,
          gtin: variant.upc || undefined,
          condition: "new", // TODO add condition if supported
          product_category: categories,
          material: variant.material || undefined,
          weight: `${variant.weight || 0} kg`,
          brand: "", // TODO add brands if supported
          color: color || undefined,
          size: size || undefined,
          seller_name: "Medusa", // TODO add seller name if supported
          seller_url: storefrontUrl || "",
          seller_privacy_policy: `${storefrontUrl}/privacy-policy`, // TODO update
          seller_tos: `${storefrontUrl}/terms-of-service`, // TODO update
          return_policy: `${storefrontUrl}/return-policy`, // TODO update
          return_window: 0, // TODO update
        })
      }
    }
  } while (count > offset)

  return new StepResponse({ items: feedItems })
})
```

In the step, you:

1. Retrieve products with pagination using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). Query allows you to retrieve data across modules. You retrieve the fields necessary for the product field.
2. For each product, you loop over its variants to add them to the product feed. You add information related to pricing, availability, and other attributes.
   - Some of the fields are hardcoded or left empty. You can update them based on your setup.
   - For more information on the fields, refer to the [Product Feed specifications](https://developers.openai.com/commerce/specs/feed).

Finally, a step function must return a `StepResponse` instance. You return the list of feed items in the response.

#### buildProductFeedXmlStep

Next, you'll create the step that generates the product feed XML from the feed items.

To create the step, create the file `src/workflows/steps/build-product-feed-xml.ts` with the following content:

```ts title="src/workflows/steps/build-product-feed-xml.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { FeedItem } from "./get-product-feed-items"

type StepInput = {
  items: FeedItem[]
}

export const buildProductFeedXmlStep = createStep(
  "build-product-feed-xml",
  async (input: StepInput) => {
    const escape = (str: string) =>
      str
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/\"/g, "&quot;")
        .replace(/'/g, "&apos;")

    const itemsXml = input.items.map((item) => {
      return (
        `<item>` +
          // Flags 
          `<enable_search>true</enable_search>` +
          `<enable_checkout>true</enable_checkout>` +
          // Product Variant Fields
          `<id>${escape(item.id)}</id>` +
          `<title>${escape(item.title)}</title>` +
          `<description>${escape(item.description)}</description>` +
          `<link>${escape(item.link)}</link>` +
          `<gtin>${escape(item.gtin || "")}</gtin>` +
          (item.image_link ? `<image_link>${escape(item.image_link)}</image_link>` : "") +
          (item.additional_image_link ? `<additional_image_link>${escape(item.additional_image_link)}</additional_image_link>` : "") +
          `<availability>${escape(item.availability)}</availability>` +
          `<inventory_quantity>${item.inventory_quantity}</inventory_quantity>` +
          `<price>${escape(item.price)}</price>` +
          (item.sale_price ? `<sale_price>${escape(item.sale_price)}</sale_price>` : "") +
          `<condition>${escape(item.condition || "new")}</condition>` +
          `<product_category>${escape(item.product_category || "")}</product_category>` +
          `<brand>${escape(item.brand || "Medusa")}</brand>` +
          `<material>${escape(item.material || "")}</material>` +
          `<weight>${escape(item.weight || "")}</weight>` +
          `<item_group_id>${escape(item.item_group_id)}</item_group_id>` +
          `<item_group_title>${escape(item.item_group_title)}</item_group_title>` +
          `<size>${escape(item.size || "")}</size>` +
          `<color>${escape(item.color || "")}</color>` +
          `<seller_name>${escape(item.seller_name)}</seller_name>` +
          `<seller_url>${escape(item.seller_url)}</seller_url>` +
          `<seller_privacy_policy>${escape(item.seller_privacy_policy)}</seller_privacy_policy>` +
          `<seller_tos>${escape(item.seller_tos)}</seller_tos>` +
          `<return_policy>${escape(item.return_policy)}</return_policy>` +
          `<return_window>${item.return_window}</return_window>` +
        `</item>`
      )
    }).join("")

    const xml =
      `<?xml version="1.0" encoding="UTF-8"?>` +
      `<rss version="2.0" xmlns:g="http://base.google.com/ns/1.0">` +
        `<channel>` +
          `<title>Product Feed</title>` +
          `<description>Product Feed for Agentic Commerce</description>` +
          itemsXml +
        `</channel>` +
      `</rss>`

    return new StepResponse(xml)
  }
)
```

This step receives the list of feed items as input.

In the step, you loop over the feed items and generate an XML string matching the [Agentic Commerce specifications](https://developers.openai.com/commerce/specs/feed). You escape special characters in the fields to ensure the XML is valid.

Finally, you return the XML string in a `StepResponse` instance.

#### sendProductFeedStep

The final step is `sendProductFeedStep`, which sends product feed XML to AI agents.

Create the file `src/workflows/steps/send-product-feed.ts` with the following content:

If you get a type error on resolving the Agentic Commerce Module, run the Medusa application once with the `npm run dev` or `yarn dev` command to generate the necessary type definitions, as explained in the [Automatically Generated Types guide](https://docs.medusajs.com/docs/learn/fundamentals/generated-types/index.html.md).

```ts title="src/workflows/steps/send-product-feed.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { AGENTIC_COMMERCE_MODULE } from "../../modules/agentic-commerce"

type StepInput = {
  productFeed: string
}

export const sendProductFeedStep = createStep(
  "send-product-feed",
  async (input: StepInput, { container }) => {
    const agenticCommerceModuleService = container.resolve(
      AGENTIC_COMMERCE_MODULE
    )

    await agenticCommerceModuleService.sendProductFeed(input.productFeed)

    return new StepResponse(void 0)
  }
)
```

This step receives the product feed XML as input.

In the step, you resolve the Agentic Commerce Module's service from the Medusa container and call its `sendProductFeed` method to send the product feed to the AI agent.

#### Create Workflow

You can now create the workflow that uses the steps you created.

Create the file `src/workflows/send-product-feed.ts` with the following content:

```ts title="src/workflows/send-product-feed.ts"
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { getProductFeedItemsStep } from "./steps/get-product-feed-items"
import { buildProductFeedXmlStep } from "./steps/build-product-feed-xml"
import { sendProductFeedStep } from "./steps/send-product-feed"

type GenerateProductFeedWorkflowInput = {
  currency_code: string
  country_code: string
}

export const sendProductFeedWorkflow = createWorkflow(
  "send-product-feed",
  (input: GenerateProductFeedWorkflowInput) => {
    const { items: feedItems } = getProductFeedItemsStep(input)

    const xml = buildProductFeedXmlStep({ 
      items: feedItems,
    })

    sendProductFeedStep({
      productFeed: xml,
    })

    return new WorkflowResponse({ xml })
  }
)

export default sendProductFeedWorkflow
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is the currency and country codes.

In the workflow, you:

1. Retrieve feed items using `getProductFeedItemsStep`.
2. Generate product feed XML using `buildProductFeedXmlStep`.
3. Send product feed to AI agents using `sendProductFeedStep`.

Finally, a workflow function must return a `WorkflowResponse` instance. You return the product feed XML in the response.

### b. Schedule Job to Send Product Feed

Next, you'll create a [scheduled job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md) that executes the `sendProductFeedWorkflow` every fifteen minutes. A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime.

Create the file `src/jobs/sync-product-feed.ts` with the following content:

```ts title="src/jobs/sync-product-feed.ts"
import {
  MedusaContainer,
} from "@medusajs/framework/types"
import sendProductFeedWorkflow from "../workflows/send-product-feed"

export default async function syncProductFeed(container: MedusaContainer) {
  const logger = container.resolve("logger")
  const query = container.resolve("query")

  const { data: regions } = await query.graph({
    entity: "region",
    fields: ["id", "currency_code", "countries.*"],
  })

  for (const region of regions) {
    for (const country of region.countries) {
      await sendProductFeedWorkflow(container).run({
        input: {
          currency_code: region.currency_code,
          country_code: country!.iso_2,
        },
      })
    }
  }

  logger.info("Product feed synced for all regions and countries")
}

export const config = {
  name: "sync-product-feed",
  schedule: "*/15 * * * *", // Every 15 minutes
}
```

In a scheduled job file, you must export:

1. An asynchronous function that holds the job's logic. The function receives the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) as a parameter.
2. A `config` object that specifies the job name and schedule. The schedule is a [cron expression](https://crontab.guru/) that defines the interval at which the job runs.

In the scheduled job function, you use Query to retrieve regions in your Medusa application, including their countries and currency codes.

Then, for each country in each region, you execute `sendProductFeedWorkflow`, passing the region's currency code and the country's ISO 2 code as input.

### Use the Scheduled Job

To use the scheduled job, start the Medusa application with the following command:

```bash npm2yarn
npm run dev
```

This job runs every fifteen minutes. The current implementation only logs product feeds to the console. Once you apply for [Instant Checkout](https://chatgpt.com/merchants), you can implement logic to send product feeds in the `sendProductFeed` method of the Agentic Commerce Module's service.

***

## Step 4: Create Checkout Session API

In this step, you'll start creating [Agentic Checkout APIs](https://developers.openai.com/commerce/specs/checkout#rest-endpoints) to handle checkout requests from AI agents.

You'll implement the `POST /checkout_sessions` API route for creating checkout sessions. AI agents call this endpoint when customers want to purchase products. This is equivalent to creating a new cart in Medusa.

To implement this API route, you'll create:

1. A workflow that prepares checkout session responses based on [Agentic Commerce specifications](https://developers.openai.com/commerce/specs/checkout#rest-endpoints). You'll use this workflow in other checkout-related workflows.
2. A workflow that creates checkout sessions.
3. An API route at `POST /checkout_sessions` that executes the workflow to create checkout sessions.
4. A middleware to authenticate AI agent requests to checkout APIs.
5. A custom error handler to return errors in the format required by [Agentic Commerce specifications](https://developers.openai.com/commerce/specs/checkout#object-definitions).

### a. Prepare Checkout Session Response Workflow

First, you'll create a workflow that prepares checkout session responses. This workflow will be used in other checkout-related workflows to return checkout sessions in the format required by [Agentic Commerce specifications](https://developers.openai.com/commerce/specs/checkout#rest-endpoints).

The workflow has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart details
- [listShippingOptionsForCartWithPricingWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow/index.html.md): Retrieve shipping options for cart

These steps and workflows are available in Medusa out-of-the-box. So, you can implement the workflow without creating custom steps.

To create the workflow, create the file `src/workflows/prepare-checkout-session-data.ts` with the following content:

```ts title="src/workflows/prepare-checkout-session-data.ts" collapsibleLines="1-10" expandButtonLabel="Show Imports"
import { 
  createWorkflow, 
  transform, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  listShippingOptionsForCartWithPricingWorkflow, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"

export type PrepareCheckoutSessionDataWorkflowInput = {
  buyer?: {
    first_name: string
    email: string
    phone_number?: string
  }
  fulfillment_address?: {
    name: string
    line_one: string
    line_two?: string
    city: string
    state: string
    postal_code: string
    phone_number?: string
    country: string
  }
  cart_id: string
  messages?: {
    type: "error" | "info"
    code: "missing" | "invalid" | "out_of_stock" | "payment_declined" | "required_sign_in" | "requires_3d"
    content_type: "plain" | "markdown"
    content: string
  }[]
}

export const prepareCheckoutSessionDataWorkflow = createWorkflow(
  "prepare-checkout-session-data",
  (input: PrepareCheckoutSessionDataWorkflowInput) => {
    // TODO add steps
  }
)
```

The `prepareCheckoutSessionDataWorkflow` accepts input with the following properties:

- `buyer`: Buyer information received from AI agents.
- `fulfillment_address`: Fulfillment address information received from AI agents.
- `cart_id`: Cart ID in Medusa, which is also the checkout session ID.
- `messages`: Messages to include in checkout session responses. This is useful for sending error or info messages to AI agents.

Next, you'll implement the workflow's logic. Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/prepare-checkout-session-data.ts"
const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: [
    "id", 
    "items.*", 
    "shipping_address.*", 
    "shipping_methods.*",
    "region.*",
    "region.payment_providers.*", 
    "currency_code", 
    "email", 
    "phone", 
    "payment_collection.*",
    "total",
    "subtotal",
    "tax_total",
    "discount_total",
    "original_item_total",
    "shipping_total",
    "metadata",
    "order.id",
  ],
  filters: {
    id: input.cart_id,
  },
  options: {
    throwIfKeyNotFound: true,
  },
})

// Retrieve shipping options
const shippingOptions = listShippingOptionsForCartWithPricingWorkflow.runAsStep({
  input: {
    cart_id: carts[0].id,
  },
})

// TODO prepare response
```

You first retrieve the cart using `useQueryGraphStep`, including fields necessary to prepare checkout session responses.

Then, you retrieve shipping options that can be used for the cart using `listShippingOptionsForCartWithPricingWorkflow`.

Next, you'll prepare checkout session response data. Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/prepare-checkout-session-data.ts"
const responseData = transform({
  input,
  carts,
  shippingOptions,
}, (data) => {
  // @ts-ignore
  const hasStripePaymentProvider = data.carts[0].region?.payment_providers?.some((provider) => provider?.id.includes("stripe"))
  const hasPaymentSession = data.carts[0].payment_collection?.payment_sessions?.some((session) => session?.status === "pending")
  return {
    id: data.carts[0].id,
    buyer: data.input.buyer,
    payment_provider: {
      provider: hasStripePaymentProvider ? "stripe" : undefined,
      supported_payment_methods: hasStripePaymentProvider ? ["card"] : undefined,
    },
    status: hasPaymentSession ? "ready_for_payment" : 
      data.carts[0].metadata?.checkout_session_canceled ? "canceled" : 
      data.carts[0].order?.id ? "completed" : "not_ready_for_payment",
    currency: data.carts[0].currency_code,
    line_items: data.carts[0].items.map((item) => ({
      id: item?.id,
      title: item?.title,
      // @ts-ignore
      base_amount: item?.original_total,
      // @ts-ignore
      discount: item?.discount_total,
      // @ts-ignore
      subtotal: item?.subtotal,
      // @ts-ignore
      tax: item?.tax_total,
      // @ts-ignore
      total: item?.total,
      item: {
        id: item?.variant_id,
        quantity: item?.quantity,
      },
    })),
    fulfillment_address: data.input.fulfillment_address,
    fulfillment_options: data.shippingOptions?.map((option) => ({
      type: "shipping",
      id: option?.id,
      title: option?.name,
      subtitle: "",
      carrier_info: option?.provider?.id,
      earliest_delivery_time: option?.type.code === "express" ? 
        new Date(Date.now() + 1 * 24 * 60 * 60 * 1000).toISOString() :  // RFC 3339 string - 24 hours
        new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(), // RFC 3339 string - 48 hours
      latest_delivery_time: option?.type.code === "express" ? 
        new Date(Date.now() + 1 * 24 * 60 * 60 * 1000).toISOString() :  // RFC 3339 string - 24 hours
        new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(), // RFC 3339 string - 48 hours
      subtotal: option?.calculated_price.calculated_amount,
      // @ts-ignore
      tax: data.carts[0].shipping_methods?.[0]?.tax_total || 0,
      // @ts-ignore
      total: data.carts[0].shipping_methods?.[0]?.total || option?.calculated_price.calculated_amount,
    })),
    fulfillment_option_id: data.carts[0].shipping_methods?.[0]?.shipping_option_id,
    totals: [
      {
        type: "item_base_amount",
        display_name: "Item Base Amount",
        // @ts-ignore
        amount: data.carts[0].original_item_total,
      },
      {
        type: "subtotal",
        display_name: "Subtotal",
        // @ts-ignore
        amount: data.carts[0].subtotal,
      },
      {
        type: "discount",
        display_name: "Discount",
        // @ts-ignore
        amount: data.carts[0].discount_total,
      },
      {
        type: "fulfillment",
        display_name: "Fulfillment",
        // @ts-ignore
        amount: data.carts[0].shipping_total,
      },
      {
        type: "tax",
        display_name: "Tax",
        // @ts-ignore
        amount: data.carts[0].tax_total,
      },
      {
        type: "total",
        display_name: "Total",
        // @ts-ignore
        amount: data.carts[0].total,
      },
    ],
    messages: data.input.messages || [],
    links: [
      {
        type: "terms_of_use",
        value: "https://www.medusa-commerce.com/terms-of-use", // TODO: replace with actual terms of use
      },
      {
        type: "privacy_policy",
        value: "https://www.medusa-commerce.com/privacy-policy", // TODO: replace with actual privacy policy
      },
      {
        type: "seller_shop_policy",
        value: "https://www.medusa-commerce.com/seller-shop-policy", // TODO: replace with actual seller shop policy
      },
    ],
  }
})

return new WorkflowResponse(responseData)
```

To create variables in workflows, you must use the [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) function. This function accepts data to manipulate as the first parameter and a transformation function as the second parameter.

In the transformation function, you prepare checkout session responses matching [Agentic Commerce response specifications](https://developers.openai.com/commerce/specs/checkout#rest-endpoints). You can replace hardcoded values with dynamic values based on your setup.

Finally, you return response data in a `WorkflowResponse` instance.

### b. Create Checkout Session Workflow

Next, you'll create a workflow that creates carts for checkout sessions. The `POST /checkout_sessions` API route will execute this workflow.

The workflow has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart details
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve variant details to validate existence
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve region details
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve sales channel details
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve customer if it exists.
- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md): Create cart for checkout session
- [prepareCheckoutSessionDataWorkflow](#prepareCheckoutSessionDataWorkflow): Prepare the checkout session response

These steps and workflows are available in Medusa out-of-the-box. So, you can implement the workflow without creating custom steps.

Create the file `src/workflows/create-checkout-session.ts` with the following content:

```ts title="src/workflows/create-checkout-session.ts" collapsibleLines="1-20" expandButtonLabel="Show Imports"
import { 
  createWorkflow, 
  transform, 
  when, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  acquireLockStep,
  addShippingMethodToCartWorkflow, 
  createCartWorkflow, 
  CreateCartWorkflowInput, 
  createCustomersWorkflow, 
  listShippingOptionsForCartWithPricingWorkflow, 
  releaseLockStep, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { 
  prepareCheckoutSessionDataWorkflow,
} from "./prepare-checkout-session-data"

type WorkflowInput = {
  items: {
    id: string
    quantity: number
  }[]
  buyer?: {
    first_name: string
    email: string
    phone_number?: string
  }
  fulfillment_address?: {
    name: string
    line_one: string
    line_two?: string
    city: string
    state: string
    postal_code: string
    phone_number?: string
    country: string
  }
}

export const createCheckoutSessionWorkflow = createWorkflow(
  "create-checkout-session",
  (input: WorkflowInput) => {
    // TODO add steps
  }
)
```

The `createCheckoutSessionWorkflow` accepts an input with the [request body of the Create Checkout Session API](https://developers.openai.com/commerce/specs/checkout#rest-endpoints).

#### Retrieve and Validate Variants

Next, you'll start implementing the workflow's logic. You'll first validate that the variants in the input exist.

Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/create-checkout-session.ts"
// validate item IDs
const variantIds = transform({
  input,
}, (data) => {
  return data.input.items.map((item) => item.id)
})

// Will fail if any variant IDs are not found
useQueryGraphStep({
  entity: "variant",
  fields: ["id"],
  filters: {
    id: variantIds,
  },
  options: {
    throwIfKeyNotFound: true,
  },
})

// TODO retrieve region and sales channel
```

You first create a variable with the variant IDs in the input using the `transform` function.

Then, you use the `useQueryGraphStep` to retrieve the variants with the IDs. You set the `throwIfKeyNotFound` option to `true` to make the step fail if any of the variant IDs are not found.

#### Retrieve Region and Sales Channel

Next, you'll retrieve the region and sales channel. These are necessary to associate the cart with the correct region and sales channel. Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/create-checkout-session.ts"
// Find the region ID for US
const { data: regions } = useQueryGraphStep({
  entity: "region",
  fields: ["id"],
  filters: {
    countries: {
      iso_2: "us",
    },
  },
}).config({ name: "find-region" })

// get sales channel
const { data: salesChannels } = useQueryGraphStep({
  entity: "sales_channel",
  fields: ["id"],
  // You can filter by name for a specific sales channel
  // filters: {
  //   name: "Agentic Commerce"
  // }
}).config({ name: "find-sales-channel" })

// TODO retrieve or create customer
```

You retrieve the region for the US using the `useQueryGraphStep` step. Instant Checkout in ChatGPT currently only supports the US region.

You also retrieve the sales channels. You can filter the sales channels by name if you want to use a specific sales channel.

#### Retrieve or Create Customer

Next, if the AI agent provides buyer information, you'll try to retrieve or create the customer. Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/create-checkout-session.ts"
// check if customer already exists
const { data: customers } = useQueryGraphStep({
  entity: "customer",
  fields: ["id"],
  filters: {
    email: input.buyer?.email,
  },
}).config({ name: "find-customer" })

// create customer if it does not exist
const createdCustomers = when ({ customers }, ({ customers }) => 
  customers.length === 0 && !!input.buyer?.email
)
.then(() => {
  return createCustomersWorkflow.runAsStep({
    input: {
      customersData: [
        {
          email: input.buyer?.email,
          first_name: input.buyer?.first_name,
          phone: input.buyer?.phone_number,
          has_account: false,
        },
      ],
    },
  })
})

// set customer ID based on existing or created customer
const customerId = transform({
  customers,
  createdCustomers,
}, (data) => {
  return data.customers.length > 0 ? 
    data.customers[0].id : data.createdCustomers?.[0].id
})

// TODO prepare cart input and create cart
```

You first try to retrieve the customer using the `useQueryGraphStep` step, filtering by the buyer's email.

Then, to perform an action based on a condition, you use [when-then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) functions. The `when` function accepts as a first parameter the data to evaluate, and as a second parameter a function that returns a boolean.

If the `when` function returns `true`, the `then` function is executed, which also accepts a function that performs steps and returns their result.

In this case, if the customer does not exist, you create it using the `createCustomersWorkflow` workflow.

Finally, you create a variable with the customer ID, which is either the existing customer's ID or the newly created customer's ID.

#### Prepare Cart Input and Create Cart

Next, you'll prepare the input for the cart creation, then create the cart. Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/create-checkout-session.ts"
const cartInput = transform({
  input,
  regions,
  salesChannels,
  customerId,
}, (data) => {
  const splitAddressName = data.input.fulfillment_address?.name.split(" ") || []
  return {
    items: data.input.items.map((item) => ({
      variant_id: item.id,
      quantity: item.quantity,
    })),
    region_id: data.regions[0]?.id,
    email: data.input.buyer?.email,
    customer_id: data.customerId,
    shipping_address: data.input.fulfillment_address ? {
      first_name: splitAddressName[0],
      last_name: splitAddressName.slice(1).join(" "),
      address_1: data.input.fulfillment_address?.line_one,
      address_2: data.input.fulfillment_address?.line_two,
      city: data.input.fulfillment_address?.city,
      province: data.input.fulfillment_address?.state,
      postal_code: data.input.fulfillment_address?.postal_code,
      country_code: data.input.fulfillment_address?.country,
    } : undefined,
    currency_code: data.regions[0]?.currency_code,
    sales_channel_id: data.salesChannels[0]?.id,
    metadata: {
      is_checkout_session: true,
    },
  } as CreateCartWorkflowInput
})

const createdCart = createCartWorkflow.runAsStep({
  input: cartInput,
})

// TODO retrieve shipping options
```

You use the `transform` function to prepare the input for the `createCartWorkflow` workflow. You map the input properties to the cart properties.

Then, you create the cart using the `createCartWorkflow` workflow.

#### Retrieve Shipping Options and Add Shipping Method

If the AI agent provides a fulfillment address in the request body, you must select the cheapest shipping option and add it to the cart.

Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/create-checkout-session.ts"
// Select the cheapest shipping option if a fulfillment address is provided
when(input, (input) => !!input.fulfillment_address)
.then(() => {
  // Retrieve shipping options
  const shippingOptions = listShippingOptionsForCartWithPricingWorkflow.runAsStep({
    input: {
      cart_id: createdCart.id,
    },
  })

  const shippingMethodData = transform({
    createdCart,
    shippingOptions,
  }, (data) => {
    // get the cheapest shipping option
    const cheapestShippingOption = data.shippingOptions.sort(
      (a, b) => a.price - b.price
    )[0]
    return {
      cart_id: data.createdCart.id,
      options: [{
        id: cheapestShippingOption.id,
      }],
    }
  })
  acquireLockStep({
    key: createdCart.id,
    timeout: 2,
    ttl: 10,
  })
  addShippingMethodToCartWorkflow.runAsStep({
    input: shippingMethodData,
  })
  releaseLockStep({
    key: createdCart.id,
  })
})

// TODO prepare checkout session response
```

You use the `when` function to check if a fulfillment address is provided in the input. If so, you:

- Retrieve the shipping options using the [listShippingOptionsForCartWithPricingWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow/index.html.md).
- Create a variable with the cheapest shipping option using the `transform` function.
- Acquire a lock on the cart to avoid race conditions.
- Add the cheapest shipping option to the cart using the [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md).
- Release the lock on the cart.

#### Prepare Checkout Session Response

Finally, you'll prepare and return the checkout session response. Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/create-checkout-session.ts"
// Prepare response data
const responseData = prepareCheckoutSessionDataWorkflow.runAsStep({
  input: {
    buyer: input.buyer,
    fulfillment_address: input.fulfillment_address,
    cart_id: createdCart.id,
  },
})

return new WorkflowResponse(responseData)
```

You prepare the checkout session response using the `prepareCheckoutSessionDataWorkflow` workflow you created earlier. You return it as the workflow's response.

### c. Create Checkout Session API Route

Next, you'll create the [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) at `POST /checkout_sessions` that executes the `createCheckoutSessionWorkflow`.

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

Refer to the [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) to learn more about them.

So, to create an API route, create the file `src/api/checkout_sessions/route.ts` with the following content:

```ts title="src/api/checkout_sessions/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
import { z } from "zod"
import { createCheckoutSessionWorkflow } from "../../workflows/create-checkout-session"
import { MedusaError } from "@medusajs/framework/utils"

export const PostCreateSessionSchema = z.object({
  items: z.array(z.object({
    id: z.string(), // variant ID
    quantity: z.number(),
  })),
  buyer: z.object({
    first_name: z.string(),
    email: z.string(),
    phone_number: z.string().optional(),
  }).optional(),
  fulfillment_address: z.object({
    name: z.string(),
    line_one: z.string(),
    line_two: z.string().optional(),
    city: z.string(),
    state: z.string(),
    country: z.string(),
    postal_code: z.string(),
    phone_number: z.string().optional(),
  }).optional(),
})

export const POST = async (
  req: MedusaRequest<
    z.infer<typeof PostCreateSessionSchema>
  >,
  res: MedusaResponse
) => {
  const logger = req.scope.resolve("logger")
  const responseHeaders = {
    "Idempotency-Key": req.headers["idempotency-key"] as string,
    "Request-Id": req.headers["request-id"] as string,
  }
  try {
    const { result } = await createCheckoutSessionWorkflow(req.scope)
      .run({
        input: req.validatedBody,
        context: {
          idempotencyKey: req.headers["idempotency-key"] as string,
        },
      })

    res.set(responseHeaders).json(result)
  } catch (error) {
    const medusaError = error as MedusaError
    logger.error(medusaError)
    res.set(responseHeaders).json({
      messages: [
        {
          type: "error",
          code: "invalid",
          content_type: "plain",
          content: medusaError.message,
        },
      ],
    })
  }
}
```

You first define a validation schema with [Zod](https://zod.dev/) for the request body. The schema matches the [Agentic Commerce request specifications](https://developers.openai.com/commerce/specs/checkout#rest-endpoints).

Then, you export a `POST` route handler function, which exposes a `POST` API route at `/checkout_sessions`.

In the route handler, you execute the `createCheckoutSessionWorkflow`, passing the validated request body as input. You return the workflow's response as the API response.

If an error occurs, you catch it and return it in the format required by the Agentic Commerce specifications.

You also return the `Idempotency-Key` and `Request-Id` headers in the response if they are provided in the request. These headers are required by the [Agentic Commerce specifications](https://developers.openai.com/commerce/specs/checkout#rest-endpoints).

### d. Create Authentication Middleware

Next, you'll create a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) to authenticate requests to checkout APIs. The middleware will run before API route handlers and verify that requests contain valid API keys and signatures before allowing access to route handlers.

Create the file `src/api/middlewares/validate-agentic-request.ts` with the following content:

```ts title="src/api/middlewares/validate-agentic-request.ts"
import { MedusaNextFunction, MedusaRequest, MedusaResponse } from "@medusajs/framework"
import { AGENTIC_COMMERCE_MODULE } from "../../modules/agentic-commerce"

export async function validateAgenticRequest(
  req: MedusaRequest, 
  res: MedusaResponse, 
  next: MedusaNextFunction
) {
  const agenticCommerceModuleService = req.scope.resolve(AGENTIC_COMMERCE_MODULE)
  const apiKeyModuleService = req.scope.resolve("api_key")
  const signature = req.headers["signature"] as string
  const apiKey = req.headers["authorization"]?.replaceAll("Bearer ", "")

  const isTokenValid = await apiKeyModuleService.authenticate(apiKey || "")
  const isSignatureValid = !!req.body || await agenticCommerceModuleService.verifySignature({
    signature,
    payload: req.body,
  })

  if (!isTokenValid || !isSignatureValid) {
    return res.status(401).json({
      message: "Unauthorized",
    })
  }

  next()
}
```

You create the `validateAgenticRequest` middleware function that accepts request, response, and next function as parameters.

In this middleware, you:

1. Resolve services of the Agentic Commerce and [API Key](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/index.html.md) modules.
2. Validate that the API key in the `Authorization` header is valid using the API Key Module's service.
3. Validate that the signature in the `Signature` header is valid using the Agentic Commerce Module's service. If the request has no body, you skip signature validation.
4. If either the API key or signature is invalid, you return a `401 Unauthorized` response.
5. Otherwise, you call the `next` function to proceed to the next middleware or route handler.

The headers are expected based on [Agentic Commerce specifications](https://developers.openai.com/commerce/specs/checkout#rest-endpoints). You can create an API key that AI agents can use in the [Secret API Key Settings](https://docs.medusajs.com/user-guide/settings/developer/secret-api-keys/index.html.md) of the Medusa Admin dashboard.

To use this middleware, you need to apply it to checkout API routes.

You apply middlewares in the `src/api/middlewares.ts` file. Create this file with the following content:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares, 
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { validateAgenticRequest } from "./middlewares/validate-agentic-request"
import { PostCreateSessionSchema } from "./checkout_sessions/route"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/checkout_sessions*",
      middlewares: [
        validateAgenticRequest,
      ],
    },
    {
      matcher: "/checkout_sessions",
      method: ["POST"],
      middlewares: [validateAndTransformBody(PostCreateSessionSchema)],
    },
  ],
})
```

You apply the `validateAgenticRequest` middleware to all routes starting with `/checkout_sessions`.

You also apply the `validateAndTransformBody` middleware to the `POST /checkout_sessions` route to ensure request bodies include required fields.

### e. Create Custom Error Handler

Finally, you'll add a custom error handler to return errors in the format required by [Agentic Commerce specifications](https://developers.openai.com/commerce/specs/checkout#object-definitions).

To override the default error handler, you can pass the `errorHandler` property to `defineMiddlewares`. It accepts an error handler function.

In `src/api/middlewares.ts`, add the following import at the top of the file:

```ts title="src/api/middlewares.ts"
import { 
  errorHandler,
} from "@medusajs/framework/http"

const originalErrorHandler = errorHandler()
```

You import the default error handler and store it in a variable to use in your custom error handler.

Then, add the `errorHandler` property to the `defineMiddlewares` function:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  // ...
  errorHandler: (error, req, res, next) => {
    if (!req.path.startsWith("/checkout_sessions")) {
      return originalErrorHandler(error, req, res, next)
    }

    res.json({
      messages: [
        {
          type: "error",
          code: "invalid",
          content_type: "plain",
          content: error.message,
        },
      ],
    })
  },
})
```

If the request path does not start with `/checkout_sessions`, you call the original error handler to handle errors.

Otherwise, you return errors in the format required by Agentic Commerce specifications.

### Use the Checkout Session API

To use the `POST /checkout_sessions` API:

- Apply to ChatGPT's [Instant Checkout](https://chatgpt.com/merchants) and access a signature key.
- Create an API key in the [Secret API Key Settings](https://docs.medusajs.com/user-guide/settings/developer/secret-api-keys/index.html.md) of the Medusa Admin dashboard.
- Setup the API key in the Instant Checkout settings.

ChatGPT will then use these to create checkout sessions.

### Test the Create Checkout Session API Locally

To test the `POST /checkout_sessions` API locally, you'll add an API route to retrieve signatures based on payloads. This allows you to simulate signature generation that ChatGPT performs.

Create the file `src/api/signature/route.ts` with the following content:

```ts title="src/api/signature/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { AGENTIC_COMMERCE_MODULE } from "../../modules/agentic-commerce"

export const POST = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const agenticCommerceModuleService = req.scope.resolve(AGENTIC_COMMERCE_MODULE)
  const signature = await agenticCommerceModuleService.getSignature(req.body)
  res.json({ signature })
}
```

This API route accepts payloads in request bodies and returns signatures generated using the Agentic Commerce Module's service.

Then, start the Medusa application with the following command:

```bash npm2yarn
npm run dev
```

After that, send a `POST` request to `http://localhost:9000/signature` with the JSON body to create checkout sessions. For example:

Make sure you [have a region with the US added to its countries](https://docs.medusajs.com/user-guide/settings/regions#create-region/index.html.md) in your Medusa store.

```bash
curl -X POST 'http://localhost:9000/signature' \
-H 'Content-Type: application/json' \
--data-raw '{
    "items": [
        {
            "id": "variant_01K6CQ43RA0RSWW1BXM8C63YT6",
            "quantity": 1
        }
    ],
    "fulfillment_address": {
        "name": "John Smith",
        "line_one": "US",
        "city": "New York",
        "state": "NY",
        "country": "us",
        "postal_code": "12345"
    },
    "buyer": {
        "email": "johnsmith@gmail.com",
        "first_name": "John",
        "phone_number": "123"
    }
}'
```

Make sure to replace the variant ID with an actual variant ID from your store.

Copy the signature from the response.

Finally, send a `POST` request to `http://localhost:9000/checkout_sessions` with the same JSON body and include the `Authorization` and `Signature` headers:

```bash
curl -X POST 'http://localhost:9000/checkout_sessions' \
-H 'Signature: {your_signature}' \
-H 'Idempotency-Key: idp_123' \
-H 'Request-Id: req_123' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {your_api_key}' \
--data-raw '{
    "items": [
        {
            "id": "variant_01K6CQ43RA0RSWW1BXM8C63YT6",
            "quantity": 1
        }
    ],
    "fulfillment_address": {
        "name": "John Smith",
        "line_one": "US",
        "city": "New York",
        "state": "NY",
        "country": "us",
        "postal_code": "12345"
    },
    "buyer": {
        "email": "johnsmith@gmail.com",
        "first_name": "John",
        "phone_number": "123"
    }
}'
```

Make sure to replace:

- `{your_signature}` with the signature you copied from the previous request.
- `{your_api_key}` with the API key you created in the Medusa Admin dashboard.
- The variant ID with an actual variant ID from your store.

You'll receive in the response the checkout session based on the Agentic Commerce specifications.

***

## Step 5: Update Checkout Session API

Next, you'll implement the `POST /checkout_sessions/{id}` API to update a checkout session.

This API is called by the AI agent to update the checkout session's details, such as when the buyer changes the fulfillment address. Whenever the checkout session is updated, you must also reset the cart's payment sessions, as instructed in the [Agentic Commerce specifications](https://developers.openai.com/commerce/specs/checkout#checkout-session).

Similar to before, you'll create:

- A workflow that updates the checkout session.
- An API route that executes the workflow.
  - You'll also apply a validation middleware to the API route.

### a. Update Checkout Session Workflow

First, you'll create the workflow that updates a checkout session. This workflow will be executed by the `POST /checkout_sessions/{id}` API route.

The workflow has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart details
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve customer if it exists.
- [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md): Acquire a lock on the cart to avoid concurrent modifications.
- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md): Update the cart with the new data
- [prepareCheckoutSessionDataWorkflow](#prepareCheckoutSessionDataWorkflow): Prepare the checkout session response
- [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md): Release lock on the cart

These steps and workflows are available in Medusa out-of-the-box. So, you can implement the workflow without creating custom steps.

Create the file `src/workflows/update-checkout-session.ts` with the following content:

```ts title="src/workflows/update-checkout-session.ts" collapsibleLines="1-18" expandButtonLabel="Show Imports"
import { 
  createWorkflow, 
  transform, 
  when, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  acquireLockStep,
  addShippingMethodToCartWorkflow, 
  createCustomersWorkflow, 
  releaseLockStep, 
  updateCartWorkflow, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { 
  prepareCheckoutSessionDataWorkflow,
} from "./prepare-checkout-session-data"

type WorkflowInput = {
  cart_id: string
  buyer?: {
    first_name: string
    email: string
    phone_number?: string
  }
  items?: {
    id: string
    quantity: number
  }[]
  fulfillment_address?: {
    name: string
    line_one: string
    line_two?: string
    city: string
    state: string
    postal_code: string
    phone_number?: string
    country: string
  }
  fulfillment_option_id?: string
}

export const updateCheckoutSessionWorkflow = createWorkflow(
  "update-checkout-session",
  (input: WorkflowInput) => {
    // Retrieve cart
    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      fields: ["id", "customer.*", "email"],
      filters: {
        id: input.cart_id,
      },
    })

    // TODO retrieve or create customer
  }
)
```

The `updateCheckoutSessionWorkflow` accepts an input with the [request body of the Update Checkout Session API](https://developers.openai.com/commerce/specs/checkout#rest-endpoints) along with the cart ID.

So far, you retrieve the cart using the `useQueryGraphStep` step.

#### Retrieve or Create Customer (Update)

Next, you'll retrieve the customer if it exists or create it if it doesn't. Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/update-checkout-session.ts"
// check if customer already exists
const { data: customers } = useQueryGraphStep({
  entity: "customer",
  fields: ["id"],
  filters: {
    email: input.buyer?.email,
  },
}).config({ name: "find-customer" })

const createdCustomers = when({ customers }, ({ customers }) => 
  customers.length === 0 && !!input.buyer?.email
)
.then(() => {
  return createCustomersWorkflow.runAsStep({
    input: {
      customersData: [
        {
          email: input.buyer?.email,
          first_name: input.buyer?.first_name,
          phone: input.buyer?.phone_number,
        },
      ],
    },
  })
})

const customerId = transform({
  customers,
  createdCustomers,
}, (data) => {
  return data.customers.length > 0 ? 
    data.customers[0].id : data.createdCustomers?.[0].id
})

// TODO validate variants if items are provided
```

You first try to retrieve the customer using the `useQueryGraphStep` step, filtering by the buyer's email.

Then, if the customer does not exist, you create it using the `createCustomersWorkflow` workflow.

Finally, you create a variable with the customer ID, which is either the existing customer's ID or the newly created customer's ID.

#### Validate Variants if Items are Provided

Next, you'll validate that the variants in the input exist if items are provided. Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/update-checkout-session.ts"
// validate items
when(input, (input) => !!input.items)
.then(() => {
  const variantIds = transform(input, (input) => input.items?.map((item) => item.id))
  return useQueryGraphStep({
    entity: "variant",
    fields: ["id"],
    filters: {
      id: variantIds,
    },
    options: {
      throwIfKeyNotFound: true,
    },
  }).config({ name: "find-variant" })
})

// TODO update cart
```

You use the `when` function to check if items are provided in the input. If so, you retrieve the variants with the IDs using the `useQueryGraphStep` step. You set the `throwIfKeyNotFound` option to `true` to make the step fail if any of the variant IDs are not found.

#### Update Cart

Next, you'll update the cart based on the input. Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/update-checkout-session.ts"
acquireLockStep({
  key: input.cart_id,
  timeout: 2,
  ttl: 10,
})
// Prepare update data
const updateData = transform({
  input,
  carts,
  customerId,
}, (data) => {
  return {
    id: data.carts[0].id,
    email: data.input.buyer?.email || data.carts[0].email,
    customer_id: data.customerId || data.carts[0].customer?.id,
    items: data.input.items?.map((item) => ({
      variant_id: item.id,
      quantity: item.quantity,
    })),
    shipping_address: data.input.fulfillment_address ? {
      first_name: data.input.fulfillment_address.name.split(" ")[0],
      last_name: data.input.fulfillment_address.name.split(" ")[1],
      address_1: data.input.fulfillment_address.line_one,
      address_2: data.input.fulfillment_address.line_two,
      city: data.input.fulfillment_address.city,
      province: data.input.fulfillment_address.state,
      postal_code: data.input.fulfillment_address.postal_code,
      country_code: data.input.fulfillment_address.country,
      phone: data.input.fulfillment_address.phone_number,
    } : undefined,
  }
})

updateCartWorkflow.runAsStep({
  input: updateData,
})

// TODO add shipping method if fulfillment option ID is provided
```

First, you acquire a lock on the cart to avoid concurrent modifications.

Then, you use the `transform` function to prepare the input for the `updateCartWorkflow` workflow. You map the input properties to the cart properties.

After that, you update the cart using the `updateCartWorkflow`. This workflow will also clear the cart's payment sessions.

#### Add Shipping Method if Fulfillment Option ID is Provided

Finally, you'll add the shipping method to the cart if a fulfillment option ID is provided, and prepare and return the checkout session response. Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/update-checkout-session.ts"
// try to update shipping method
when(input, (input) => !!input.fulfillment_option_id)
.then(() => {
  addShippingMethodToCartWorkflow.runAsStep({
    input: {
      cart_id: updateData.id,
      options: [{
        id: input.fulfillment_option_id!,
      }],
    },
  })
})

const responseData = prepareCheckoutSessionDataWorkflow.runAsStep({
  input: {
    cart_id: updateData.id,
    buyer: input.buyer,
    fulfillment_address: input.fulfillment_address,
  },
})

releaseLockStep({
  key: input.cart_id,
})

return new WorkflowResponse(responseData)
```

You use the `when` function to check if a fulfillment option ID is provided in the input. If it is, you add it to the cart using the `addShippingMethodToCartWorkflow` workflow.

Then, you prepare the checkout session response using the `prepareCheckoutSessionDataWorkflow` workflow you created earlier.

Finally, you release the lock on the cart and return the prepared response as the workflow's response.

### b. Update Checkout Session API Route

Next, you'll create an API route that executes the `updateCheckoutSessionWorkflow`.

Create the file `src/api/checkout_sessions/[id]/route.ts` with the following content:

```ts title="src/api/checkout_sessions/[id]/route.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
import { updateCheckoutSessionWorkflow } from "../../../workflows/update-checkout-session"
import { z } from "zod"
import { MedusaError } from "@medusajs/framework/utils"
import { prepareCheckoutSessionDataWorkflow } from "../../../workflows/prepare-checkout-session-data"
import { refreshPaymentCollectionForCartWorkflow } from "@medusajs/medusa/core-flows"

export const PostUpdateSessionSchema = z.object({
  buyer: z.object({
    first_name: z.string(),
    email: z.string(),
    phone_number: z.string().optional(),
  }).optional(),
  items: z.array(z.object({
    id: z.string(),
    quantity: z.number(),
  })).optional(),
  fulfillment_address: z.object({
    name: z.string(),
    line_one: z.string(),
    line_two: z.string().optional(),
    city: z.string(),
    state: z.string(),
    country: z.string(),
    postal_code: z.string(),
    phone_number: z.string().optional(),
  }).optional(),
  fulfillment_option_id: z.string().optional(),
})

export const POST = async (
  req: MedusaRequest<
    z.infer<typeof PostUpdateSessionSchema>
  >,
  res: MedusaResponse
) => {
  const responseHeaders = {
    "Idempotency-Key": req.headers["idempotency-key"] as string,
    "Request-Id": req.headers["request-id"] as string,
  }
  try {
    const { result } = await updateCheckoutSessionWorkflow(req.scope)
      .run({
        input: {
          cart_id: req.params.id,
          ...req.validatedBody,
        },
        context: {
          idempotencyKey: req.headers["idempotency-key"] as string,
        },
      })

    res.set(responseHeaders).json(result)
  } catch (error) {
    const medusaError = error as MedusaError

    await refreshPaymentCollectionForCartWorkflow(req.scope).run({
      input: {
        cart_id: req.params.id,
      },
    })
    
    const { result } = await prepareCheckoutSessionDataWorkflow(req.scope)
      .run({
        input: {
          cart_id: req.params.id,
          ...req.validatedBody,
          messages: [
            {
              type: "error",
              code: medusaError.type === MedusaError.Types.PAYMENT_AUTHORIZATION_ERROR ? 
                "payment_declined" : "invalid",
              content_type: "plain",
              content: medusaError.message,
            },
          ],
        },
      })

    res.set(responseHeaders).json(result)
  }
}
```

You first define a validation schema with [Zod](https://zod.dev/) for the request body. The schema matches the [Agentic Commerce request specifications](https://developers.openai.com/commerce/specs/checkout#rest-endpoints).

Then, you export a `POST` route handler function, which exposes a `POST` API route at `/checkout_sessions/{id}`.

In the route handler, you execute the `updateCheckoutSessionWorkflow`, passing the cart ID from the URL parameters and the validated request body as input. You return the workflow's response as the API response.

If an error occurs, you refresh the cart's payment sessions using the `refreshPaymentCollectionForCartWorkflow`, and prepare the checkout session response with an error message using the `prepareCheckoutSessionDataWorkflow` workflow. You return this response.

### c. Apply Validation Middleware

Finally, you'll apply the validation middleware to the `POST /checkout_sessions/{id}` API route.

In `src/api/middlewares.ts`, add the following import at the top of the file:

```ts title="src/api/middlewares.ts"
import { PostUpdateSessionSchema } from "./checkout_sessions/[id]/route"
```

Then, add a new route configuration in `defineMiddlewares`:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/checkout_sessions/:id",
      method: ["POST"],
      middlewares: [validateAndTransformBody(PostUpdateSessionSchema)],
    },
  ],
  // ...
})
```

You apply the `validateAndTransformBody` middleware to the `POST /checkout_sessions/{id}` route to ensure the request body includes the required fields.

### Use the Update Checkout Session API

To use the `POST /checkout_sessions/:id` API, you need to:

- Apply to ChatGPT's [Instant Checkout](https://chatgpt.com/merchants) and access a signature key.
- Create an API key in the [Secret API Key Settings](https://docs.medusajs.com/user-guide/settings/developer/secret-api-keys/index.html.md) of the Medusa Admin dashboard.
- Setup the API key in the Instant Checkout settings.

ChatGPT will then use it to update a checkout session.

### Test the Update Checkout Session API Locally

To test out the `POST /checkout_sessions/:id` API locally, you need to use the same signature generation API route you created earlier.

First, assuming you already [created a checkout session](#test-the-create-checkout-session-api-locally) and have the cart ID, send a `POST` request to `http://localhost:9000/signature` with the JSON body to update the checkout session. For example:

```bash
curl -X POST 'http://localhost:9000/signature' \
-H 'Content-Type: application/json' \
--data '{
    "fulfillment_option_id": "so_01K6FCGVFMNNC5H43SB9NNNAJ3"
}'
```

Make sure to replace the fulfillment option ID with an actual shipping option ID from your store.

Then, send a `POST` request to `http://localhost:9000/checkout_sessions/{cart_id}` with the same JSON body, and include the `Authorization` and `Signature` headers:

```bash
curl -X POST 'http://localhost:9000/checkout_sessions/{cart_id}' \
-H 'Signature: {signature}' \
-H 'Idempotency-Key: idp_123' \
-H 'Request-Id: req_123' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {api_key}' \
--data '{
    "fulfillment_option_id": "so_01K6FCGVFMNNC5H43SB9NNNAJ3"
}'
```

Make sure to replace:

- `{cart_id}` with the cart ID of the checkout session you created earlier.
- `{signature}` with the signature you copied from the previous request.
- `{api_key}` with the API key you created in the Medusa Admin dashboard.
- The fulfillment option ID with an actual shipping option ID from your store.

You'll receive in the response the updated checkout session based on the Agentic Commerce specifications.

***

## Step 6: Get Checkout Session API

Next, you'll implement the `GET /checkout_sessions/{id}` API to retrieve a checkout session. This API is called by the AI agent to get the current state of the checkout session.

This API route will use the `prepareCheckoutSessionDataWorkflow` workflow you created earlier to prepare the checkout session response.

To create the API route, add the following function to `src/api/checkout_sessions/[id]/route.ts`:

```ts title="src/api/checkout_sessions/[id]/route.ts"
export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const responseHeaders = {
    "Idempotency-Key": req.headers["idempotency-key"] as string,
    "Request-Id": req.headers["request-id"] as string,
  }
  try {
    const { result } = await prepareCheckoutSessionDataWorkflow(req.scope)
      .run({
        input: {
          cart_id: req.params.id,
        },
        context: {
          idempotencyKey: req.headers["idempotency-key"] as string,
        },
      })

    res.set(responseHeaders).status(200).json(result)
  } catch (error) {
    const medusaError = error as MedusaError
    const statusCode = medusaError.type === MedusaError.Types.NOT_FOUND ? 404 : 500
    res.set(responseHeaders).status(statusCode).json({
      type: "invalid_request",
      code: "request_not_idempotent",
      message: statusCode === 404 ? "Checkout session not found" : "Internal server error",
    })
  }
}
```

You export a `GET` route handler function, which exposes a `GET` API route at `/checkout_sessions/{id}`.

In the route handler, you execute the `prepareCheckoutSessionDataWorkflow`, passing the cart ID from the URL parameters as input. You return the workflow's response as the API response.

### Use the Get Checkout Session API

To use the `GET /checkout_sessions/:id` API, you need to:

- Apply to ChatGPT's [Instant Checkout](https://chatgpt.com/merchants) and access a signature key.
- Create an API key in the [Secret API Key Settings](https://docs.medusajs.com/user-guide/settings/developer/secret-api-keys/index.html.md) of the Medusa Admin dashboard.
- Setup the API key in the Instant Checkout settings.

ChatGPT will then use it to get a checkout session.

### Test the Get Checkout Session API Locally

To test out the `GET /checkout_sessions/:id` API locally, send a `GET` request to `/checkout_sessions/{cart_id}`:

```bash
curl 'http://localhost:9000/checkout_sessions/{cart_id}' \
-H 'Idempotency-Key: idp_123' \
-H 'Request-Id: req_123' \
-H 'Authorization: Bearer {api_key}'
```

Make sure to replace:

- `{cart_id}` with the cart ID of the checkout session you created earlier.
- `{api_key}` with the API key you created in the Medusa Admin dashboard.

You'll receive in the response the checkout session based on the Agentic Commerce specifications.

***

## Step 7: Complete Checkout Session API

Next, you'll implement the `POST /checkout_sessions/{id}/complete` API to complete a checkout session.

The AI agent calls this API route to finalize the checkout process and create an order. This API route will complete the cart, process the payment, and return the final checkout session details. If an error occurs, it resets the cart's payment sessions and returns the checkout session with an error message.

To implement this API route, you'll create:

- A workflow that completes the checkout session.
- An API route that executes the workflow.
  - You'll also apply a validation middleware to the API route.

You'll also set up the [Stripe Payment Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md). ChatGPT currently supports only Stripe as a payment provider.

### a. Complete Checkout Session Workflow

The workflow that completes a checkout session has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart details
- [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md): Acquire a lock on the cart to prevent concurrent modifications
- [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md): Release the lock on the cart

These steps and workflows are available in Medusa out-of-the-box. So, you can implement the workflow without creating custom steps.

To create the workflow, create the file `src/workflows/complete-checkout-session.ts` with the following content:

```ts title="src/workflows/complete-checkout-session.ts" collapsibleLines="1-21" expandButtonLabel="Show Imports"
import { 
  createWorkflow, 
  transform, 
  when, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  acquireLockStep,
  completeCartWorkflow, 
  createPaymentCollectionForCartWorkflow, 
  createPaymentSessionsWorkflow, 
  refreshPaymentCollectionForCartWorkflow, 
  releaseLockStep, 
  updateCartWorkflow, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { 
  prepareCheckoutSessionDataWorkflow, 
  PrepareCheckoutSessionDataWorkflowInput,
} from "./prepare-checkout-session-data"

type WorkflowInput = {
  cart_id: string
  buyer?: {
    first_name: string
    email: string
    phone_number?: string
  }
  payment_data: {
    token: string
    provider: string
    billing_address?: {
      name: string
      line_one: string
      line_two?: string
      city: string
      state: string
      postal_code: string
      country: string
      phone_number?: string
    }
  }
}

export const completeCheckoutSessionWorkflow = createWorkflow(
  "complete-checkout-session",
  (input: WorkflowInput) => {
    // Retrieve cart details
    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      fields: [
        "id", 
        "region.*", 
        "region.payment_providers.*", 
        "shipping_address.*",
      ],
      filters: {
        id: input.cart_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })
    acquireLockStep({
      key: input.cart_id,
      timeout: 2,
      ttl: 10,
    })

    // TODO update cart with billing address if provided
  }
)
```

The `completeCheckoutSessionWorkflow` accepts an input with the properties received from the AI agent to complete the checkout session.

So far, you retrieve the cart using the `useQueryGraphStep`, and you acquire a lock on the cart using the `acquireLockStep` to prevent concurrent modifications.

#### Update Cart with Billing Address

Next, you'll update the cart with the billing address if it's provided. Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/complete-checkout-session.ts"
when(input, (input) => !!input.payment_data.billing_address)
.then(() => {
  const updateData = transform({
    input,
    carts,
  }, (data) => {
    return {
      id: data.carts[0].id,
      billing_address: {
        first_name: data.input.payment_data.billing_address!.name.split(" ")[0],
        last_name: data.input.payment_data.billing_address!.name.split(" ")[1],
        address_1: data.input.payment_data.billing_address!.line_one,
        address_2: data.input.payment_data.billing_address!.line_two,
        city: data.input.payment_data.billing_address!.city,
        province: data.input.payment_data.billing_address!.state,
        postal_code: data.input.payment_data.billing_address!.postal_code,
        country_code: data.input.payment_data.billing_address!.country,
        phone: data.input.payment_data.billing_address!.phone_number,
      },
    }
  })
  return updateCartWorkflow.runAsStep({
    input: updateData,
  })
})

// TODO complete cart if payment provider is valid
```

You use the `when` function to check if a billing address is provided in the input. If it is, you prepare the input for the `updateCartWorkflow` workflow using the `transform` function, and then you update the cart using the `updateCartWorkflow` workflow.

#### Complete Cart if Payment Provider is Valid

Next, you'll complete the cart if the payment provider is valid. Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/complete-checkout-session.ts"
const preparationInput = transform({
  carts,
  input,
}, (data) => {
  return {
    cart_id: data.carts[0].id,
    buyer: data.input.buyer,
    fulfillment_address: data.carts[0].shipping_address ? {
      name: data.carts[0].shipping_address.first_name + " " + data.carts[0].shipping_address.last_name,
      line_one: data.carts[0].shipping_address.address_1 || "",
      line_two: data.carts[0].shipping_address.address_2 || "",
      city: data.carts[0].shipping_address.city || "",
      state: data.carts[0].shipping_address.province || "",
      postal_code: data.carts[0].shipping_address.postal_code || "",
      country: data.carts[0].shipping_address.country_code || "",
      phone_number: data.carts[0].shipping_address.phone || "",
    } : undefined,
  }
})

const paymentProviderId = transform({
  input,
}, (data) => {
  switch (data.input.payment_data.provider) {
    case "stripe":
      return "pp_stripe_stripe"
    default:
      return data.input.payment_data.provider
  }
})

const completeCartResponse = when({
  carts,
  paymentProviderId,      
}, (data) => {
  // @ts-ignore
  return !!data.carts[0].region?.payment_providers?.find((provider) => provider?.id === data.paymentProviderId)
})
.then(() => {
  const paymentCollection = createPaymentCollectionForCartWorkflow.runAsStep({
    input: {
      cart_id: carts[0].id,
    },
  })

  createPaymentSessionsWorkflow.runAsStep({
    input: {
      payment_collection_id: paymentCollection.id,
      provider_id: paymentProviderId,
      data: {
        shared_payment_token: input.payment_data.token,
      },
    },
  })

  completeCartWorkflow.runAsStep({
    input: {
      id: carts[0].id,
    },
  })

  return prepareCheckoutSessionDataWorkflow.runAsStep({
    input: preparationInput,
  })
})

// TODO handle invalid payment provider
```

You use `transform` to prepare the input for the `prepareCheckoutSessionDataWorkflow` workflow and to map the payment provider from the input to the payment provider ID used in your Medusa store.

Then, you use the `when` function to check if the payment provider is valid for the cart's region. If so, you:

- Create a payment collection for the cart using the [createPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentCollectionForCartWorkflow/index.html.md).
- Create payment sessions in the payment collection using the [createPaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentSessionsWorkflow/index.html.md).
- Complete the cart using the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md).

Finally, you prepare the checkout session response using the `prepareCheckoutSessionDataWorkflow`.

#### Handle Invalid Payment Provider

Next, you'll handle the case where the payment provider is invalid. You'll prepare an error response to return to the AI agent.

Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/complete-checkout-session.ts"
const invalidPaymentResponse = when({
  carts,
  paymentProviderId,
}, (data) => {
  return !data.carts[0].region?.payment_providers?.find((provider) => provider?.id === data.paymentProviderId)
})
.then(() => {
  refreshPaymentCollectionForCartWorkflow.runAsStep({
    input: {
      cart_id: carts[0].id,
    },
  })
  const prepareDataWithMessages = transform({
    prepareData: preparationInput,
  }, (data) => {
    return {
      ...data.prepareData,
      messages: [
        {
          type: "error",
          code: "invalid",
          content_type: "plain",
          content: "Invalid payment provider",
        },
      ],
    } as PrepareCheckoutSessionDataWorkflowInput
  })
  return prepareCheckoutSessionDataWorkflow.runAsStep({
    input: prepareDataWithMessages,
  }).config({ name: "prepare-checkout-session-data-with-messages" })
})

// Return response
```

You use the `when` function to check if the payment provider is invalid for the cart's region.

If so, you refresh the cart's payment sessions using the `refreshPaymentCollectionForCartWorkflow`, then you prepare the input for the `prepareCheckoutSessionDataWorkflow` workflow. You add an error message indicating that the payment provider is invalid.

#### Return Response

Finally, you'll return the appropriate response based on whether the cart was completed or if there was an error. Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/complete-checkout-session.ts"
const responseData = transform({
  completeCartResponse,
  invalidPaymentResponse,
}, (data) => {
  return data.completeCartResponse || data.invalidPaymentResponse
})

releaseLockStep({
  key: input.cart_id,
})

return new WorkflowResponse(responseData)
```

You use `transform` to pick either the response from completing the cart or the error response for an invalid payment provider.

Then, you release the lock on the cart using the `releaseLockStep` step, and return the response as the workflow's response.

### b. Complete Checkout Session API Route

Next, you'll create an API route at `POST /checkout_sessions/{id}/complete` that executes the `completeCheckoutSessionWorkflow`.

Create the file `src/api/checkout_sessions/[id]/complete/route.ts` with the following content:

```ts title="src/api/checkout_sessions/[id]/complete/route.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
import { z } from "zod"
import { completeCheckoutSessionWorkflow } from "../../../../workflows/complete-checkout-session"
import { MedusaError } from "@medusajs/framework/utils"
import { refreshPaymentCollectionForCartWorkflow } from "@medusajs/medusa/core-flows"
import { prepareCheckoutSessionDataWorkflow } from "../../../../workflows/prepare-checkout-session-data"

export const PostCompleteSessionSchema = z.object({
  buyer: z.object({
    first_name: z.string(),
    email: z.string(),
    phone_number: z.string().optional(),
  }).optional(),
  payment_data: z.object({
    token: z.string(),
    provider: z.string(),
    billing_address: z.object({
      name: z.string(),
      line_one: z.string(),
      line_two: z.string().optional(),
      city: z.string(),
      state: z.string(),
      postal_code: z.string(),
      country: z.string(),
      phone_number: z.string().optional(),
    }).optional(),
  }),
})

export const POST = async (
  req: MedusaRequest<
    z.infer<typeof PostCompleteSessionSchema>
  >,
  res: MedusaResponse
) => {
  const responseHeaders = {
    "Idempotency-Key": req.headers["idempotency-key"] as string,
    "Request-Id": req.headers["request-id"] as string,
  }
  try {
    const { result } = await completeCheckoutSessionWorkflow(req.scope)
      .run({
        input: {
          cart_id: req.params.id,
          ...req.validatedBody,
        },
        context: {
          idempotencyKey: req.headers["idempotency-key"] as string,
        },
      })

    res.set(responseHeaders).json(result)
  } catch (error) {
    const medusaError = error as MedusaError

    await refreshPaymentCollectionForCartWorkflow(req.scope).run({
      input: {
        cart_id: req.params.id,
      },
    })    
    const { result } = await prepareCheckoutSessionDataWorkflow(req.scope)
      .run({
        input: {
          cart_id: req.params.id,
          ...req.validatedBody,
          messages: [
            {
              type: "error",
              code: medusaError.type === MedusaError.Types.PAYMENT_AUTHORIZATION_ERROR ? 
                "payment_declined" : "invalid",
              content_type: "plain",
              content: medusaError.message,
            },
          ],
        },
      })

    res.set(responseHeaders).json(result)
  }
}
```

You first define a validation schema with [Zod](https://zod.dev/) for the request body. The schema matches the [Agentic Commerce request specifications](https://developers.openai.com/commerce/specs/checkout#rest-endpoints).

Then, you export a `POST` route handler function, which exposes a `POST` API route at `/checkout_sessions/{id}/complete`.

In the route handler, you execute the `completeCheckoutSessionWorkflow`, passing the cart ID from the URL parameters and the validated request body as input. You return the workflow's response as the API response.

If an error occurs, you refresh the cart's payment sessions using the `refreshPaymentCollectionForCartWorkflow`, and prepare the checkout session response with an error message using the `prepareCheckoutSessionDataWorkflow`. You return this response.

### c. Apply Validation Middleware

Finally, you'll apply the validation middleware to the `POST /checkout_sessions/{id}/complete` API route.

In `src/api/middlewares.ts`, add the following import at the top of the file:

```ts title="src/api/middlewares.ts"
import { PostCompleteSessionSchema } from "./checkout_sessions/[id]/complete/route"
```

Then, add a new route configuration in `defineMiddlewares`:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/checkout_sessions/:id/complete",
      method: ["POST"],
      middlewares: [validateAndTransformBody(PostCompleteSessionSchema)],
    },
  ],
  // ...
})
```

You apply the `validateAndTransformBody` middleware to the `POST /checkout_sessions/{id}/complete` route to ensure the request body includes the required fields.

### d. Setup Stripe Payment Module Provider

To support payments with Stripe, you'll need to set up the [Stripe Payment Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) in your Medusa store.

In `medusa-config.ts`, add a new entry to the `modules` array:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    // ...
    {
      resolve: "@medusajs/medusa/payment",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/payment-stripe",
            id: "stripe",
            options: {
              apiKey: process.env.STRIPE_API_KEY,
              // other options...
            },
          },
        ],
      },
    },
  ],
})
```

This will add Stripe as a payment provider in your Medusa store.

Make sure to set the `STRIPE_API_KEY` environment variable with your Stripe secret key, which you can retrieve from the [Stripe Dashboard](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard):

```env title=".env"
STRIPE_API_KEY=sk_test_...
```

Finally, you must enable Stripe as a payment provider in the US region. Learn how to do that in the [Regions user guide](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details/index.html.md).

### Use the Complete Checkout Session API

To use the `POST /checkout_sessions/:id/complete` API, you need to:

- Apply to ChatGPT's [Instant Checkout](https://chatgpt.com/merchants) and access a signature key.
- Create an API key in the [Secret API Key Settings](https://docs.medusajs.com/user-guide/settings/developer/secret-api-keys/index.html.md) of the Medusa Admin dashboard.
- Setup the API key in the Instant Checkout settings.

ChatGPT will then use it to complete a checkout session.

### Test the Complete Checkout Session API Locally

To test out the `POST /checkout_sessions/:id/complete` API locally, you need to [generate a shared payment token with Stripe](https://docs.stripe.com/agentic-commerce/testing).

Then, send a `POST` request to `http://localhost:9000/signature` with the JSON body to complete the checkout session. For example:

```bash
curl -X POST 'http://localhost:9000/signature' \
-H 'Content-Type: application/json' \
--data-raw '{
    "buyer": {
        "first_name": "John",
        "email": "johnsmith@gmail.com",
        "phone_number": "123"
    },
    "payment_data": {
        "provider": "stripe",
        "token": "{token}"
    }
}'
```

Make sure to replace `{token}` with the shared payment token you generated with Stripe.

Then, send a `POST` request to `http://localhost:9000/checkout_sessions/{cart_id}/complete` with the same JSON body, and include the `Authorization` and `Signature` headers:

```bash
curl -X POST 'http://localhost:9000/checkout_sessions/{cart_id}/complete' \
-H 'Signature: {signature}' \
-H 'Idempotency-Key: idp_123' \
-H 'Request-Id: req_123' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {api_key}' \
--data-raw '{
    "buyer": {
        "first_name": "John",
        "email": "johnsmith@gmail.com",
        "phone_number": "123"
    },
    "payment_data": {
        "provider": "stripe",
        "token": "{token}"
    }
}'
```

Make sure to replace:

- `{cart_id}` with the cart ID of the checkout session you created earlier.
- `{signature}` with the signature you copied from the previous request.
- `{token}` with the shared payment token you generated with Stripe.
- `{api_key}` with the API key you created in the Medusa Admin dashboard.

You'll receive in the response the completed checkout session based on the Agentic Commerce specifications.

***

## Step 8: Cancel Checkout Session API

The last Checkout Session API you'll implement is the `POST /checkout_sessions/{id}/cancel` API to cancel a checkout session.

The AI agent calls this API route to cancel the checkout process. This API route will cancel any authorized payment sessions associated with the cart, update the cart status to canceled, and return the updated checkout session details.

To implement this API route, you'll create:

- A workflow that cancels the checkout session.
- An API route that executes the workflow.

### a. Cancel Checkout Session Workflow

The workflow that cancels a checkout session has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart details
- [validateCartCancelationStep](#validateCartCancelationStep): Validate if the cart can be canceled
- [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md): Acquire a lock on the cart to prevent concurrent modifications
- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md): Update the cart status to canceled
- [prepareCheckoutSessionDataWorkflow](#prepareCheckoutSessionDataWorkflow): Prepare the checkout session response
- [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md): Release the lock on the cart

You only need to implement the `validateCartCancelationStep` and `cancelPaymentSessionsStep` steps. The other steps and workflows are available in Medusa out-of-the-box.

#### validateCartCancelationStep

The `validateCartCancelationStep` step throws an error if the cart cannot be canceled.

To create the step, create the file `src/workflows/steps/validate-cart-cancelation.ts` with the following content:

```ts title="src/workflows/steps/validate-cart-cancelation.ts"
import { CartDTO, OrderDTO, PaymentCollectionDTO } from "@medusajs/framework/types"
import { MedusaError } from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk"

export type ValidateCartCancelationStepInput = {
  cart: CartDTO & {
    payment_collection?: PaymentCollectionDTO
    order?: OrderDTO
  }
}

export const validateCartCancelationStep = createStep(
  "validate-cart-cancelation",
  async ({ cart }: ValidateCartCancelationStepInput) => {
    if (cart.metadata?.checkout_session_canceled) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Cart is already canceled"
      )
    }
    if (!!cart.order) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Cart is already associated with an order"
      )
    }
    const invalidPaymentSessions = cart.payment_collection?.payment_sessions
      ?.some((session) => session.status === "authorized" || session.status === "canceled")

    if (!!cart.completed_at || !!invalidPaymentSessions) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Cart cannot be canceled"
      )
    }
  }
)
```

The `validateCartCancelationStep` accepts a cart as input.

In the step, you throw an error if:

- The cart has a `checkout_session_canceled` metadata field set to `true`, indicating it was already canceled.
- The cart is already associated with an order.
- The cart has a `completed_at` date, indicating it was already completed.
- The cart has any payment sessions with a status of `authorized` or `canceled`.

#### cancelPaymentSessionsStep

The `cancelPaymentSessionsStep` step cancels payment sessions associated with the cart.

To create the step, create the file `src/workflows/steps/cancel-payment-sessions.ts` with the following content:

```ts title="src/workflows/steps/cancel-payment-sessions.ts"
import { promiseAll } from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

type StepInput = {
  payment_session_ids: string[]
}

export const cancelPaymentSessionsStep = createStep(
  "cancel-payment-session",
  async ({ payment_session_ids }: StepInput, { container }) => {
    const paymentModuleService = container.resolve("payment")

    const paymentSessions = await paymentModuleService.listPaymentSessions({
      id: payment_session_ids,
    })

    const updatedPaymentSessions = await promiseAll(
      paymentSessions.map((session) => {
        return paymentModuleService.updatePaymentSession({
          id: session.id,
          status: "canceled",
          currency_code: session.currency_code,
          amount: session.amount,
          data: session.data,
        })
      })
    )

    return new StepResponse(updatedPaymentSessions, paymentSessions)
  },
  async (paymentSessions, { container }) => {
    if (!paymentSessions) {
      return
    }
    const paymentModuleService = container.resolve("payment")

    await promiseAll(
      paymentSessions.map((session) => {
        return paymentModuleService.updatePaymentSession({
          id: session.id,
          status: session.status,
          currency_code: session.currency_code,
          amount: session.amount,
          data: session.data,
        })
      })
    )
  }
)
```

The `cancelPaymentSessionsStep` accepts an array of payment session IDs as input.

In the step, you retrieve the payment sessions using the `listPaymentSessions` method of the [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md)'s service. Then, you update their status to `canceled` using the `updatePaymentSession` method.

You also pass a third argument to the `createStep` function, which is the [compensation function](https://docs.medusajs.com/docs/learn/fundamentals/workflows/compensation-function/index.html.md). This function is executed if the workflow execution fails, allowing you to revert any changes made by the step. In this case, you revert the payment sessions to their original status.

#### Cancel Checkout Session Workflow

You can now implement the `cancelCheckoutSessionWorkflow`.

Create the file `src/workflows/cancel-checkout-session.ts` with the following content:

```ts title="src/workflows/cancel-checkout-session.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { validateCartCancelationStep, ValidateCartCancelationStepInput } from "./steps/validate-cart-cancelation"
import { acquireLockStep, releaseLockStep, updateCartWorkflow, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { cancelPaymentSessionsStep } from "./steps/cancel-payment-sessions"
import { prepareCheckoutSessionDataWorkflow } from "./prepare-checkout-session-data"

type WorkflowInput = {
  cart_id: string
}

export const cancelCheckoutSessionWorkflow = createWorkflow(
  "cancel-checkout-session",
  (input: WorkflowInput) => {
    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      fields: [
        "id", 
        "payment_collection.*", 
        "payment_collection.payment_sessions.*",
        "order.id",
      ],
      filters: {
        id: input.cart_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    validateCartCancelationStep({
      cart: carts[0],
    } as unknown as ValidateCartCancelationStepInput)

    acquireLockStep({
      key: input.cart_id,
      timeout: 2,
      ttl: 10,
    })

    // TODO cancel payment sessions if any
  }
)
```

The `cancelCheckoutSessionWorkflow` accepts an input with the cart ID of the checkout session to cancel.

So far, you:

1. Retrieve the cart using the `useQueryGraphStep` step.
2. Validate that the cart can be canceled using the `validateCartCancelationStep`.
3. Acquire a lock on the cart using the `acquireLockStep` to prevent concurrent modifications.

Next, you'll cancel the payment sessions if there are any. Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/cancel-checkout-session.ts"
when({
  carts,
}, (data) => !!data.carts[0].payment_collection?.payment_sessions?.length)
.then(() => {
  const paymentSessionIds = transform({
    carts,
  }, (data) => {
    return data.carts[0].payment_collection?.payment_sessions?.map((session) => session!.id)
  })
  cancelPaymentSessionsStep({
    payment_session_ids: paymentSessionIds,
  })
})

updateCartWorkflow.runAsStep({
  input: {
    id: carts[0].id,
    metadata: {
      checkout_session_canceled: true,
    },
  },
})

// TODO prepare and return response
```

You use the `when` function to check if the cart has any payment sessions. If so, you prepare an array with the payment session IDs using the `transform` function and then you cancel the payment sessions using the `cancelPaymentSessionsStep`.

You also update the cart using the `updateCartWorkflow` workflow to add a `checkout_session_canceled` metadata field to the cart. This is useful to detect canceled checkout sessions in the future.

Finally, you'll prepare the checkout session response, release the lock, and return the response. Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/cancel-checkout-session.ts"
const responseData = prepareCheckoutSessionDataWorkflow.runAsStep({
  input: {
    cart_id: carts[0].id,
  },
})

releaseLockStep({
  key: input.cart_id,
})

return new WorkflowResponse(responseData)
```

You prepare the checkout session response using the `prepareCheckoutSessionDataWorkflow` workflow, then you release the lock on the cart using the `releaseLockStep`. Finally, you return the response.

### b. Cancel Checkout Session API Route

Next, you'll create a `POST` API route at `/checkout_sessions/{id}/cancel` that executes the `cancelCheckoutSessionWorkflow`.

Create the file `src/api/checkout_sessions/[id]/cancel/route.ts` with the following content:

```ts title="src/api/checkout_sessions/[id]/cancel/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { cancelCheckoutSessionWorkflow } from "../../../../workflows/cancel-checkout-session"
import { MedusaError } from "@medusajs/framework/utils"

export const POST = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const responseHeaders = {
    "Idempotency-Key": req.headers["idempotency-key"] as string,
    "Request-Id": req.headers["request-id"] as string,
  }
  try {
    const { result } = await cancelCheckoutSessionWorkflow(req.scope)
      .run({
        input: {
          cart_id: req.params.id,
        },
        context: {
          idempotencyKey: req.headers["idempotency-key"] as string,
        },
      })
    
    res.set(responseHeaders).json(result)
  } catch (error) {
    const medusaError = error as MedusaError
    res.set(responseHeaders).status(405).json({
      messages: [
        {
          type: "error",
          code: "invalid",
          content_type: "plain",
          content: medusaError.message,
        },
      ],
    })
  }
}
```

You export a `POST` route handler function, which exposes a `POST` API route at `/checkout_sessions/{id}/cancel`.

In the route handler, you execute the `cancelCheckoutSessionWorkflow`, passing the cart ID from the URL parameters as input. You return the workflow's response as the API response.

### Use the Cancel Checkout Session API

To use the `POST /checkout_sessions/:id/cancel` API, you need to:

- Apply to ChatGPT's [Instant Checkout](https://chatgpt.com/merchants) and access a signature key.
- Create an API key in the [Secret API Key Settings](https://docs.medusajs.com/user-guide/settings/developer/secret-api-keys/index.html.md) of the Medusa Admin dashboard.
- Setup the API key in the Instant Checkout settings.

ChatGPT will then use it to cancel a checkout session.

### Test the Cancel Checkout Session API Locally

To test out the `POST /checkout_sessions/:id/cancel` API locally, send a `POST` request to `/checkout_sessions/{cart_id}/cancel`:

```bash
curl -X POST 'http://localhost:9000/checkout_sessions/{cart_id}/cancel' \
-H 'Idempotency-Key: idp_123' \
-H 'Request-Id: req_123' \
-H 'Authorization: Bearer {api_key}'
```

Make sure to replace:

- `{cart_id}` with the cart ID of the checkout session you created earlier.
- `{api_key}` with the API key you created in the Medusa Admin dashboard.

You'll receive in the response the canceled checkout session based on the Agentic Commerce specifications.

***

## Step 9: Send Webhook Events to AI Agent

In the last step, you'll send webhook events to AI agents when orders are placed or updated. This informs AI agents about order updates, as specified in [Agentic Commerce specifications](https://developers.openai.com/commerce/specs/checkout#webhooks).

To send webhook events to AI agents on order updates, you'll create a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). A subscriber is an asynchronous function that listens to events to perform tasks.

In this case, you'll create a subscriber that listens to `order.placed` and `order.updated` events to send webhook events to AI agents.

Create the file `src/subscribers/order-webhooks.ts` with the following content:

```ts title="src/subscribers/order-webhooks.ts"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { AGENTIC_COMMERCE_MODULE } from "../modules/agentic-commerce"
import { AgenticCommerceWebhookEvent } from "../modules/agentic-commerce/service"

export default async function orderWebhookHandler({
  event: { data, name },
  container,
}: SubscriberArgs<{ id: string }>) {
  const orderId = data.id
  const query = container.resolve("query")
  const agenticCommerceModuleService = container.resolve(AGENTIC_COMMERCE_MODULE)
  const configModule = container.resolve("configModule")
  const storefrontUrl = configModule.admin.storefrontUrl || process.env.STOREFRONT_URL

  // retrieve order
  const { data: [order] } = await query.graph({
    entity: "order",
    fields: [
      "id",
      "cart.id",
      "cart.metadata",
      "status",
      "fulfillments.*",
      "transactions.*",
    ],
    filters: {
      id: orderId,
    },
  })

  // only send webhook if order is associated with a checkout session
  if (!order || !order.cart?.metadata?.is_checkout_session) {
    return
  }

  // prepare webhook event
  const webhookEvent: AgenticCommerceWebhookEvent = {
    type: name === "order.placed" ? "order.created" : "order.updated",
    data: {
      type: "order",
      checkout_session_id: order.cart.id,
      permalink_url: `${storefrontUrl}/orders/${order.id}`,
      status: "confirmed",
      refunds: order.transactions?.filter(
        (transaction) => transaction?.reference === "refund"
      ).map((transaction) => ({
        type: "original_payment",
        amount: transaction!.amount * -1,
      })) || [],
    },
  }

  // set status based on order, fulfillments and transactions
  if (order.status === "canceled") {
    webhookEvent.data.status = "canceled"
  } else {
    const allFulfillmentsShipped = order.fulfillments?.every((fulfillment) => !!fulfillment?.shipped_at)
    const allFulfillmentsDelivered = order.fulfillments?.every((fulfillment) => !!fulfillment?.delivered_at)
    if (allFulfillmentsShipped) {
      webhookEvent.data.status = "shipping"
    } else if (allFulfillmentsDelivered) {
      webhookEvent.data.status = "fulfilled"
    }
  }

  // send webhook event
  await agenticCommerceModuleService.sendWebhookEvent(webhookEvent)
}

export const config: SubscriberConfig = {
  event: ["order.placed", "order.updated"],
}
```

A subscriber file must export:

1. An asynchronous function, which is the subscriber that executes when events are emitted.
2. A configuration object that holds names of events the subscriber listens to, which are `order.placed` and `order.updated` in this case.

The subscriber function receives an object as a parameter that has a `container` property, which is the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

In the subscriber function, you:

- Retrieve orders using Query. You filter by order IDs received in event data.
- If orders don't exist or if order carts don't have the `is_checkout_session` metadata field, you return early since orders are not associated with checkout sessions.
- Prepare webhook event payloads based on order data, following [Agentic Commerce webhook specifications](https://developers.openai.com/commerce/specs/checkout#webhooks).
- Send webhook events to AI agents using the `sendWebhookEvent` method of the Agentic Commerce Module's service.

### Use Order Webhook Events in ChatGPT

To use order webhook events in ChatGPT:

- Apply to ChatGPT's [Instant Checkout](https://chatgpt.com/merchants) and access a signature key.
- Set up webhook URLs in Instant Checkout settings and update `sendWebhookEvent` to use webhook URLs from the settings.

ChatGPT will then receive webhook events when orders are placed or updated.

### Test Order Webhook Events Locally

To test order webhook events locally and ensure they're being sent correctly:

1. Start the Medusa server.
2. [Create a checkout session](#test-the-create-checkout-session-api-locally).
3. [Complete the checkout session](#test-the-complete-checkout-session-api-locally).
4. Check the logs of your Medusa server to see that `order.placed` events were emitted and webhook events were sent to AI agents.

***

## Next Steps

You've now built Agentic Commerce integration in your Medusa store. You can use it once you apply to ChatGPT's [Instant Checkout](https://chatgpt.com/merchants) and set up the integration in Instant Checkout settings.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get more in-depth understanding of all concepts used in this guide and more.

To learn more about commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.


# Add Images to Product Categories

In this tutorial, you'll learn how to add images to product categories in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with the Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md), which are available out of the box.

Medusa doesn't natively support adding images to product categories. However, it provides the customization capabilities you need to implement this feature.

## Summary

By following this tutorial, you'll learn how to:

- Install and set up Medusa with the Next.js Starter Storefront.
- Define data models for product category images and the logic to manage them.
- Customize the Medusa Admin dashboard to manage category images.
- Customize the Next.js Starter Storefront to add a megamenu that shows category thumbnails, and show a banner image on category pages.

![Diagram showing the relation between product categories and their images](https://res.cloudinary.com/dza7lstvk/image/upload/v1760522310/Medusa%20Resources/category-images-summary_l1duwj.jpg)

- [Full Code](https://github.com/medusajs/examples/tree/main/category-images): Find the full code for this tutorial in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1760540368/OpenApi/category-images.openapi_azg6xy.yaml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.

Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Product Media Module

In Medusa, you can build custom features in a [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a reusable package with the data models and functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more.

In this step, you'll build a Product Media module that manages images for product categories. You can also extend it to manage images for other product-related entities, such as product collections.

### a. Create Module Directory

Create the directory `src/modules/product-media` that will hold the Product Media Module's code.

### b. Create Data Models

A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

Refer to the [Data Models](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md) documentation to learn more.

For the Product Media module, you only need the `ProductCategoryImage` data model to represent an image associated with a product category.

To create the data model, create the file `src/modules/product-media/models/product-category-image.ts` with the following content:

```ts title="src/modules/product-media/models/product-category-image.ts" highlights={dataModelHighlights}
import { model } from "@medusajs/framework/utils"

const ProductCategoryImage = model.define("product_category_image", {
  id: model.id().primaryKey(),
  url: model.text(),
  file_id: model.text(),
  type: model.enum(["thumbnail", "image"]),
  category_id: model.text(),
})
  .indexes([
    {
      on: ["category_id", "type"],
      where: "type = 'thumbnail'",
      unique: true,
      name: "unique_thumbnail_per_category",
    },
  ])

export default ProductCategoryImage
```

The `ProductCategoryImage` data model has the following properties:

- `id`: A unique identifier for the image.
- `url`: The URL of the image.
- `file_id`: The ID of the file in the external storage service. This is useful when deleting the file from storage.
- `type`: The type of image, which can be either `thumbnail` or `image`.
- `category_id`: The ID of the product category associated with this image.

You also define a unique index on the `category_id` and `type` columns to ensure each product category has only one thumbnail image.

Learn more about defining data model properties in the [Property Types documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md).

### c. Create Module's Service

You manage your module's data models in a service.

A service is a TypeScript class that the module exports. In the service's methods, you can connect to the database to manage your data models, or connect to third-party services when integrating with external platforms.

Refer to the [Module Service documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md) to learn more.

To create the Product Media module's service, create the file `src/modules/product-media/service.ts` with the following content:

```ts title="src/modules/product-media/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import ProductCategoryImage from "./models/product-category-image"

class ProductMediaModuleService extends MedusaService({
  ProductCategoryImage,
}) {}

export default ProductMediaModuleService
```

The `ProductMediaModuleService` extends `MedusaService`, which generates a class with data-management methods for your module's data models. This saves you time implementing Create, Read, Update, and Delete (CRUD) methods.

The `ProductMediaModuleService` class now has methods like `createProductCategoryImages` and `retrieveProductCategoryImages`.

Find all methods generated by the `MedusaService` in [the Service Factory](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md) reference.

### d. Create the Module Definition

The final piece of a module is its definition, which you export in an `index.ts` file at the module's root directory. This definition tells Medusa the module's name and its service.

So, create the file `src/modules/product-media/index.ts` with the following content:

```ts title="src/modules/product-media/index.ts"
import ProductMediaModuleService from "./service"
import { Module } from "@medusajs/framework/utils"

export const PRODUCT_MEDIA_MODULE = "productMedia"

export default Module(PRODUCT_MEDIA_MODULE, {
  service: ProductMediaModuleService,
})
```

You use the `Module` function to create the module's definition. It accepts two parameters:

1. The module's name, which is `productMedia`.
2. An object with a required property `service` indicating the module's service.

You also export the module's name as `PRODUCT_MEDIA_MODULE` so you can reference it later.

### Add Module to Medusa's Configurations

After building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property with an array containing your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/product-media",
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

### Generate Migrations

Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript class that defines database changes made by a module.

Refer to the [Migrations documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md) to learn more.

Medusa's CLI tool can generate migrations for you. To generate a migration for the Product Media Module, run the following command in your Medusa application's directory:

```bash
npx medusa db:generate productMedia
```

The `db:generate` command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a `migrations` directory under `src/modules/product-media` that holds the generated migration.

Then, to reflect these migrations on the database, run the following command:

```bash
npx medusa db:migrate
```

The tables for the data models are now created in the database.

***

## Step 3: Create Product Category Images

In this step, you'll implement the logic to create product category images using the Product Media module.

When building commerce features in Medusa that client applications consume, such as the Medusa Admin dashboard or a storefront, you need to implement:

1. A [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) with steps that define the feature's business logic.
2. An [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) that exposes the workflow's functionality to client applications.

In this step, you'll create a workflow and an API route to create product category images.

You won't implement the functionality to upload the image, as Medusa already exposes an API route to upload files. You'll use that route in the Medusa Admin dashboard to upload images and get their URLs and file IDs.

### a. Create Product Category Image Workflow

In this section, you'll implement the workflow that creates product category images.

A workflow is a series of queries and actions, called steps, that complete a task. A workflow is similar to a function, but it allows you to track execution progress, define rollback logic, and configure other advanced features.

Refer to the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) to learn more.

The workflow you'll build has the following steps:

- [createCategoryImagesStep](#createCategoryImagesStep): Create the category images.

### convertCategoryThumbnailsStep

The `convertCategoryThumbnailsStep` converts existing thumbnails of a category to regular images if a new thumbnail is being added for that category. This ensures that each category has only one thumbnail image.

To create the step, create the file `src/workflows/steps/convert-category-thumbnails.ts` with the following content:

```ts title="src/workflows/steps/convert-category-thumbnails.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PRODUCT_MEDIA_MODULE } from "../../modules/product-media"
import ProductMediaModuleService from "../../modules/product-media/service"

export type ConvertCategoryThumbnailsStepInput = {
  category_ids: string[]
}

export const convertCategoryThumbnailsStep = createStep(
  "convert-category-thumbnails-step",
  async (input: ConvertCategoryThumbnailsStepInput, { container }) => {
    // TODO: implement step logic
  },
  async (compensationData, { container }) => {
    // TODO: implement compensation logic
  }
)
```

You create a step with the `createStep` function. It accepts three parameters:

1. The step's unique name.
2. An async function that receives two parameters:
   - The step's input, which is an object containing the categories whose thumbnails to convert.
   - An object with properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools accessible in the step.
3. An async compensation function that undoes the actions performed by the step function. This function executes only if an error occurs during workflow execution.

Next, define the step's logic that converts existing thumbnails of a category to regular images. Replace the `// TODO: implement step logic` comment in the step function with the following:

```ts title="src/workflows/steps/convert-category-thumbnails.ts"
const productMediaService: ProductMediaModuleService =
  container.resolve(PRODUCT_MEDIA_MODULE)

// Find existing thumbnails in the specified categories
const existingThumbnails = await productMediaService.listProductCategoryImages({
  type: "thumbnail",
  category_id: input.category_ids,
})

if (existingThumbnails.length === 0) {
  return new StepResponse([], [])
}

// Store previous states for compensation
const compensationData: string[] = existingThumbnails.map((t) => t.id)

// Convert existing thumbnails to "image" type
await productMediaService.updateProductCategoryImages(
  existingThumbnails.map((t) => ({
    id: t.id,
    type: "image" as const,
  }))
)

return new StepResponse(existingThumbnails, compensationData)
```

In the step function, you:

1. Resolve the `ProductMediaModuleService` from the Medusa container to manage product category images.
2. Retrieve existing thumbnail images for the categories in the input.
3. If there are no existing thumbnails, return an empty `StepResponse`.
4. Otherwise, update the existing thumbnails to regular images.

A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters:

1. The step's output, which is the updated category images.
2. Data to pass to the step's compensation function.

The compensation function should undo the actions performed by the step function. Replace the `// TODO: implement compensation logic` comment in the compensation function with the following:

```ts
if (!compensationData?.length) {
  return
}

const productMediaService: ProductMediaModuleService =
  container.resolve(PRODUCT_MEDIA_MODULE)

// Revert thumbnails back to "thumbnail" type
await productMediaService.updateProductCategoryImages(
  compensationData.map((id) => ({
    id,
    type: "thumbnail" as const,
  }))
)
```

In the compensation function, you revert the images back to thumbnails in case an error occurs in the workflow execution.

#### createCategoryImagesStep

The `createCategoryImagesStep` creates the category images.

To create the step, create the file `src/workflows/steps/create-category-images.ts` with the following content:

```ts title="src/workflows/steps/create-category-images.ts" highlights={createCategoryImagesStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PRODUCT_MEDIA_MODULE } from "../../modules/product-media"
import ProductMediaModuleService from "../../modules/product-media/service"
import { MedusaError } from "@medusajs/framework/utils"

export type CreateCategoryImagesStepInput = {
  category_images: {
    category_id: string
    type: "thumbnail" | "image"
    url: string
    file_id: string
  }[]
}

export const createCategoryImagesStep = createStep(
  "create-category-images-step",
  async (input: CreateCategoryImagesStepInput, { container }) => {
    const productMediaService: ProductMediaModuleService =
      container.resolve(PRODUCT_MEDIA_MODULE)

    // Group images by category to handle thumbnails efficiently
    const imagesByCategory = input.category_images.reduce((acc, img) => {
      if (!acc[img.category_id]) {
        acc[img.category_id] = []
      }
      acc[img.category_id].push(img)
      return acc
    }, {} as Record<string, typeof input.category_images>)

    // Process each category
    for (const [_, images] of Object.entries(imagesByCategory)) {
      const thumbnailImages = images.filter((img) => img.type === "thumbnail")
      
      // If there are new thumbnails for this category, convert existing ones to "image"
      if (thumbnailImages.length > 1) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          "Only one thumbnail is allowed per category"
        )
      }
    }

    // Create all category images
    const createdImages = await productMediaService.createProductCategoryImages(
      Object.values(imagesByCategory).flat()
    )

    return new StepResponse(createdImages, createdImages)
  },
  async (compensationData, { container }) => {
    if (!compensationData?.length) {
      return
    }

    const productMediaService: ProductMediaModuleService =
      container.resolve(PRODUCT_MEDIA_MODULE)

    await productMediaService.deleteProductCategoryImages(
      compensationData
    )
  }
)
```

This step accepts the category images to create as input.

In the step function, you throw an error if more than one thumbnail image is being added for a category. Otherwise, you create the category images.

In the compensation function, you delete the created category images in case an error occurs in the workflow execution.

#### Create Workflow

You can now create the workflow that uses the `createCategoryImagesStep` step.

To create the workflow, create the file `src/workflows/create-category-images.ts` with the following content:

```ts title="src/workflows/create-category-images.ts" highlights={createCategoryImagesWorkflowHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
import {
  createWorkflow,
  transform,
  when,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { createCategoryImagesStep } from "./steps/create-category-images"
import { convertCategoryThumbnailsStep } from "./steps/convert-category-thumbnails"

export type CreateCategoryImagesInput = {
  category_images: {
    category_id: string
    type: "thumbnail" | "image"
    url: string
    file_id: string
  }[]
}

export const createCategoryImagesWorkflow = createWorkflow(
  "create-category-images",
  (input: CreateCategoryImagesInput) => {

    when(input, (data) => data.category_images.some((img) => img.type === "thumbnail"))
    .then(
      () => {
        const categoryIds = transform({
          input,
        }, (data) => {
          return data.input.category_images.filter(
            (img) => img.type === "thumbnail"
          ).map((img) => img.category_id)
        })
        
        convertCategoryThumbnailsStep({
          category_ids: categoryIds,
        })
      }
    )

    const categoryImages = createCategoryImagesStep({
      category_images: input.category_images,
    })

    return new WorkflowResponse(categoryImages)
  }
)
```

You create a workflow using the `createWorkflow` function. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function that holds the workflow's implementation. The function accepts an input object with the category images to create.

In the workflow, you:

1. Check if any of the images to create is a thumbnail using [when-then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md).
   - If so, you execute the `convertCategoryThumbnailsStep` step to convert existing thumbnails of the categories to regular images.
2. Create the category images using the `createCategoryImagesStep`.

A workflow must return an instance of `WorkflowResponse` that accepts the data to return to the workflow's executor. You return the created images.

In a workflow, you can't manipulate data or check conditions because Medusa stores an internal representation of the workflow on application startup. Learn more in the [Data Manipulation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) and [Conditions](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) documentation.

### b. Create API Route

Next, you'll create an API route that exposes the workflow's functionality to client applications.

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

Refer to the [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) documentation to learn more about them.

Create the file `src/api/admin/categories/[category_id]/images/route.ts` with the following content:

```ts title="src/api/admin/categories/[category_id]/images/route.ts" highlights={createApiRouteHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { createCategoryImagesWorkflow } from "../../../../../workflows/create-category-images"
import { z } from "zod"

export const CreateCategoryImagesSchema = z.object({
  images: z.array(
    z.object({
      type: z.enum(["thumbnail", "image"]),
      url: z.string(),
      file_id: z.string(),
    })
  ).min(1, "At least one image is required"),
})

type CreateCategoryImagesInput = z.infer<typeof CreateCategoryImagesSchema>

export async function POST(
  req: MedusaRequest<CreateCategoryImagesInput>,
  res: MedusaResponse
): Promise<void> {
  const { category_id } = req.params
  const { images } = req.validatedBody

  // Add category_id to each image
  const category_images = images.map((image) => ({
    ...image,
    category_id,
  }))

  const { result } = await createCategoryImagesWorkflow(req.scope).run({
    input: {
      category_images,
    },
  })

  res.status(200).json({ category_images: result })
}
```

You create the `CreateCategoryImagesSchema` schema to validate request bodies sent to this API route using [Zod](https://zod.dev/).

Then, you export a `POST` function, which exposes a `POST` API route at `/admin/categories/:category_id/images`.

In the API route, you execute the `createCategoryImagesWorkflow` workflow with the category images to create. You set each image's `category_id` to the `category_id` parameter from the request URL.

Finally, you return the created category images in the response.

### c. Add Validation Middleware

To validate the body parameters of requests sent to the API route, apply a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

To apply middleware to a route, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares, 
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { 
  CreateCategoryImagesSchema,
} from "./admin/categories/[category_id]/images/route"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/admin/categories/:category_id/images",
      method: ["POST"],
      middlewares: [
        validateAndTransformBody(CreateCategoryImagesSchema),
      ],
    },
  ],
})
```

You apply Medusa's `validateAndTransformBody` middleware to `POST` requests sent to the `/admin/categories/:category_id/images` route. The middleware function accepts a Zod schema that you created in the API route's file.

Refer to the [Middlewares](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) documentation to learn more.

You'll test this API route later when you customize the Medusa Admin.

***

## Step 4: List Product Category Images API

In this step, you'll add an API route that retrieves a category's images.

In `src/api/admin/categories/[category_id]/images/route.ts`, add the following at the end of the file:

```ts title="src/api/admin/categories/[category_id]/images/route.ts"
export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
): Promise<void> {
  const { category_id } = req.params
  const query = req.scope.resolve("query")

  const { data: categoryImages } = await query.graph({
    entity: "product_category_image",
    fields: ["*"],
    filters: {
      category_id,
    },
  })

  res.status(200).json({ category_images: categoryImages })
}
```

You export a `GET` function that exposes a `GET` API route at `/admin/categories/:category_id/images`.

In the API route, you resolve [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which retrieves data across modules.

You use Query to retrieve the images for the category whose ID is specified in the request's URL parameters.

Finally, you return the retrieved category images in the response.

You'll test this API route next when you customize the Medusa Admin.

***

## Step 5: Create Product Category Images in Medusa Admin

In this step, you'll customize the Medusa Admin dashboard to manage a product category's images.

The Medusa Admin dashboard is customizable, allowing you to insert widgets into existing pages or create new pages.

Refer to the [Admin Development](https://docs.medusajs.com/docs/learn/fundamentals/admin/index.html.md) documentation to learn more.

In this step, you'll insert a widget into the product category details page to display its images and allow uploading new ones. Later, you'll expand the widget to support deleting images and updating their types.

### a. Initialize JS SDK

To send requests to the Medusa server, you'll use the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md). It's already installed in your Medusa project, but you need to initialize it before using it in your customizations.

Create the file `src/admin/lib/sdk.ts` with the following content:

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

Learn more about the initialization options in the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) reference.

### b. Define Types

Next, you'll define TypeScript types that you'll use in your admin customizations.

Create the file `src/admin/types.ts` with the following content:

```ts title="src/admin/types.ts"
export type CategoryImage = {
  id?: string
  url: string
  type: "thumbnail" | "image"
  file_id: string
  category_id?: string
}

export type UploadedFile = {
  id: string
  url: string
  type?: "thumbnail" | "image"
}
```

You define types for a product category image and an uploaded file (before it is created as a category image).

### c. Add Media Widget

Next, you'll add a widget to the product category details page to show its images.

Widgets are created in a `.tsx` file under the `src/admin/widgets` directory. So, create the file `src/admin/widgets/category-media-widget.tsx` with the following content:

```tsx title="src/admin/widgets/category-media-widget.tsx" highlights={categoryMediaWidgetHighlights}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"
import { DetailWidgetProps, AdminProductCategory } from "@medusajs/framework/types"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"
import { CategoryImage } from "../types"
import { ThumbnailBadge } from "@medusajs/icons"

type CategoryImagesResponse = {
  category_images: CategoryImage[]
}

const CategoryMediaWidget = ({ data }: DetailWidgetProps<AdminProductCategory>) => {
  const { data: response, isLoading } = useQuery({
    queryKey: ["category-images", data.id],
    queryFn: async () => {
      const result = await sdk.client.fetch<CategoryImagesResponse>(
        `/admin/categories/${data.id}/images`
      )
      return result
    },
  })

  const images = response?.category_images || []

  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">Media</Heading>
        {/* TODO show edit modal */}
      </div>
      <div className="px-6 py-4">
        <div className="grid grid-cols-[repeat(auto-fill,96px)] gap-4">
          {isLoading && (
            <div className="col-span-full">
              <p className="text-ui-fg-subtle text-sm">Loading...</p>
            </div>
          )}
          {!isLoading && images.length === 0 && (
            <div className="col-span-full">
              <p className="text-ui-fg-subtle text-sm">No images added yet</p>
            </div>
          )}
          {images.map((image: CategoryImage) => (
            <div
              key={image.id}
              className="relative aspect-square overflow-hidden rounded-lg border border-ui-border-base bg-ui-bg-subtle"
            >
              <img
                src={image.url}
                alt={`Category ${image.type}`}
                className="h-full w-full object-cover"
              />
              {image.type === "thumbnail" && (
                <div className="absolute top-2 left-2">
                  <ThumbnailBadge />
                </div>
              )}
            </div>
          ))}
        </div>
      </div>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product_category.details.after",
})

export default CategoryMediaWidget
```

A widget file must export:

- A default React component. This component renders the widget's UI.
- A `config` object created with `defineWidgetConfig` from the Admin SDK. It accepts an object with the `zone` property that indicates where the widget will be rendered in the Medusa Admin dashboard.

In the widget's component, you use [Tanstack (React) Query](https://tanstack.com/query/latest) to fetch the category images with the JS SDK. You display the images in a grid.

If an image is a thumbnail, you show a `ThumbnailBadge` icon at the top-left corner of the image.

### d. Category Media Modal

Next, you'll create a modal that displays the category images with a form to upload new images. Later, you'll expand on the modal to allow deleting images or updating their types.

#### Category Image Item Component

First, you'll create a component that represents a category image in the modal.

Create the file `src/admin/components/category-media/category-image-item.tsx` with the following content:

```tsx title="src/admin/components/category-media/category-image-item.tsx"
import { ThumbnailBadge } from "@medusajs/icons"

type CategoryImageItemProps = {
  id: string
  url: string
  alt: string
  isThumbnail: boolean
}

export const CategoryImageItem = ({
  id,
  url,
  alt,
  isThumbnail,
}: CategoryImageItemProps) => {
  return (
    <div
      key={id}
      className="shadow-elevation-card-rest hover:shadow-elevation-card-hover focus-visible:shadow-borders-focus bg-ui-bg-subtle-hover group relative aspect-square h-auto max-w-full overflow-hidden rounded-lg outline-none"
    >
      {isThumbnail && (
        <div className="absolute left-2 top-2">
          <ThumbnailBadge />
        </div>
      )}
      {/* TODO add selection checkbox */}
      <img
        src={url}
        alt={alt}
        className="size-full object-cover object-center"
      />
    </div>
  )
}
```

The `CategoryImageItem` component accepts the image's ID, URL, alt text, and whether it's a thumbnail. It displays the image and a `ThumbnailBadge` icon if it's a thumbnail.

#### Category Image Gallery Component

Next, you'll create a component that displays a gallery of category images, including existing and newly uploaded images.

Create the file `src/admin/components/category-media/category-image-gallery.tsx` with the following content:

```tsx title="src/admin/components/category-media/category-image-gallery.tsx"
import { Text } from "@medusajs/ui"
import { CategoryImage, UploadedFile } from "../../types"
import { CategoryImageItem } from "./category-image-item"

type CategoryImageGalleryProps = {
  existingImages: CategoryImage[]
  uploadedFiles: UploadedFile[]
  currentThumbnailId: string | null
}

export const CategoryImageGallery = ({
  existingImages,
  uploadedFiles,
  currentThumbnailId,
}: CategoryImageGalleryProps) => {
  // TODO filter deleted images
  const visibleExistingImages = existingImages

  const hasNoImages = visibleExistingImages.length === 0 && uploadedFiles.length === 0

  return (
    <div className="bg-ui-bg-subtle size-full overflow-auto">
      <div className="grid h-fit auto-rows-auto grid-cols-4 gap-6 p-6">
        {/* Existing images */}
        {visibleExistingImages.map((image) => {
          if (!image.id) {return null}
          
          const imageId = image.id
          const isThumbnail = currentThumbnailId === imageId
          
          return (
            <CategoryImageItem
              key={imageId}
              id={imageId}
              url={image.url}
              alt={`Category ${image.type}`}
              isThumbnail={isThumbnail}
            />
          )
        })}

        {/* Newly uploaded files */}
        {uploadedFiles.map((file) => {
          const uploadedId = `uploaded:${file.id}`
          const isThumbnail = currentThumbnailId === uploadedId
          
          return (
            <CategoryImageItem
              key={file.id}
              id={file.id}
              url={file.url}
              alt="Uploaded"
              isThumbnail={isThumbnail}
            />
          )
        })}

        {/* Empty state */}
        {hasNoImages && (
          <div className="col-span-4 flex items-center justify-center p-8">
            <Text className="text-ui-fg-subtle text-center">
              No images yet. Upload images to get started.
            </Text>
          </div>
        )}
      </div>
    </div>
  )
}
```

The `CategoryImageGallery` component accepts the following props:

- `existingImages`: The existing category images.
- `uploadedFiles`: The newly uploaded files that are not yet created as category images.
- `currentThumbnailId`: The ID of the current thumbnail image.

The component displays the existing images and the newly uploaded files using the `CategoryImageItem` component. It also shows an empty state message if there are no images.

#### Category Image Upload Component

Next, you'll create a component that allows uploading new images.

Create the file `src/admin/components/category-media/category-image-upload.tsx` with the following content:

```tsx title="src/admin/components/category-media/category-image-upload.tsx"
import { RefObject } from "react"
import { ArrowDownTray } from "@medusajs/icons"

type CategoryImageUploadProps = {
  fileInputRef: RefObject<HTMLInputElement>
  isUploading: boolean
  onFileSelect: (files: FileList | null) => void
}

export const CategoryImageUpload = ({
  fileInputRef,
  isUploading,
  onFileSelect,
}: CategoryImageUploadProps) => {
  return (
    <div className="bg-ui-bg-base overflow-auto border-b px-6 py-4 lg:border-b-0 lg:border-l">
      <div className="flex flex-col space-y-2">
        <div className="flex flex-col gap-y-2">
          <div className="flex flex-col gap-y-1">
            <div className="flex items-center gap-x-1">
              <label className="font-sans txt-compact-small font-medium">
                Media
              </label>
              <p className="font-normal font-sans txt-compact-small text-ui-fg-muted">
                (Optional)
              </p>
            </div>
            <span className="txt-small text-ui-fg-subtle">
              Add media to the product to showcase it in your storefront.
            </span>
          </div>

          <div>
            <input
              ref={fileInputRef}
              type="file"
              multiple
              accept="image/jpeg,image/png,image/gif,image/webp,image/heic,image/svg+xml"
              onChange={(e) => onFileSelect(e.target.files)}
              hidden
            />

            <button
              type="button"
              onClick={() => fileInputRef.current?.click()}
              disabled={isUploading}
              className="bg-ui-bg-component border-ui-border-strong transition-fg group flex w-full flex-col items-center gap-y-2 rounded-lg border border-dashed p-8 hover:border-ui-border-interactive focus:border-ui-border-interactive focus:shadow-borders-focus outline-none focus:border-solid disabled:opacity-50 disabled:cursor-not-allowed"
              onDragOver={(e) => {
                e.preventDefault()
                e.stopPropagation()
              }}
              onDrop={(e) => {
                e.preventDefault()
                e.stopPropagation()
                if (!isUploading) {
                  onFileSelect(e.dataTransfer.files)
                }
              }}
            >
              <div className="text-ui-fg-subtle group-disabled:text-ui-fg-disabled flex items-center gap-x-2">
                <ArrowDownTray />
                <p className="font-normal font-sans txt-medium">
                  {isUploading ? "Uploading..." : "Upload images"}
                </p>
              </div>
              <p className="font-normal font-sans txt-compact-small text-ui-fg-muted group-disabled:text-ui-fg-disabled">
                Drag and drop images here or click to upload.
              </p>
            </button>
          </div>
        </div>
      </div>
    </div>
  )
}
```

The `CategoryImageUpload` component accepts the following props:

- `fileInputRef`: A reference to the hidden file input element.
- `isUploading`: A boolean indicating whether files are being uploaded.
- `onFileSelect`: A callback function that is called when files are selected or dropped.

The component renders a button that opens the file picker when clicked. It also supports drag-and-drop uploads.

When files are selected or dropped, the component calls the `onFileSelect` callback with the selected files.

#### Category Image Hooks

Next, you'll create custom hooks for uploading images and creating category images. You'll use these hooks in the modal to upload images then create category images.

Create the file `src/admin/hooks/use-category-image.ts` with the following content:

```ts title="src/admin/hooks/use-category-image.ts"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"
import { CategoryImage } from "../types"

type UseCategoryImageMutationsProps = {
  categoryId: string
  onCreateSuccess?: () => void
}

export const useCategoryImageMutations = ({
  categoryId,
  onCreateSuccess,
}: UseCategoryImageMutationsProps) => {
  const queryClient = useQueryClient()

  const uploadFilesMutation = useMutation({
    mutationFn: async (files: File[]) => {
      const response = await sdk.admin.upload.create({ files })
      return response
    },
    onError: (error) => {
      console.error("Failed to upload files:", error)
    },
  })

  const createImagesMutation = useMutation({
    mutationFn: async (images: Omit<CategoryImage, "id" | "category_id">[]) => {
      const response = await sdk.client.fetch(
        `/admin/categories/${categoryId}/images`,
        {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: {
            images,
          },
        }
      )
      return response
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["category-images", categoryId] })
      onCreateSuccess?.()
    },
  })

  // TODO add update and delete mutations

  return {
    uploadFilesMutation,
    createImagesMutation,
  }
}
```

The `useCategoryImageMutations` hook accepts the following parameters:

- `categoryId`: The ID of the category to manage images for.
- `onCreateSuccess`: An optional callback function called after successfully creating images.

The hook returns two mutations:

1. `uploadFilesMutation`: A mutation that uploads files using Medusa's existing API route for uploads. This will upload the images to the [configured File Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file#what-is-a-file-module-provider/index.html.md).
2. `createImagesMutation`: A mutation that creates category images by sending a `POST` request to the API route you created earlier.

You'll later add mutations to update and delete category images.

#### Category Media Modal Component

Finally, you'll create the modal component that uses the components and hook you created earlier.

Create the file `src/admin/components/category-media/category-media-modal.tsx` with the following content:

```tsx title="src/admin/components/category-media/category-media-modal.tsx" highlights={categoryMediaModalHighlights1} collapsibleLines="1-8" expandButtonLabel="Show Imports"
import { useState, useRef } from "react"
import { FocusModal, Button, Heading, toast } from "@medusajs/ui"
import { useQueryClient } from "@tanstack/react-query"
import { CategoryImage, UploadedFile } from "../../types"
import { CategoryImageGallery } from "./category-image-gallery"
import { CategoryImageUpload } from "./category-image-upload"
import { useCategoryImageMutations } from "../../hooks/use-category-image"

type CategoryMediaModalProps = {
  categoryId: string
  existingImages: CategoryImage[]
}

export const CategoryMediaModal = ({
  categoryId,
  existingImages,
}: CategoryMediaModalProps) => {
  const [open, setOpen] = useState(false)
  const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([])
  const [currentThumbnailId, setCurrentThumbnailId] = useState<string | null>(
    null
  )
  const fileInputRef = useRef<HTMLInputElement>(null)
  const queryClient = useQueryClient()

  const {
    uploadFilesMutation,
    createImagesMutation,
  } = useCategoryImageMutations({
    categoryId,
    onCreateSuccess: () => {
      setOpen(false)
      resetModalState()
    },
  })

  const isSaving = 
    createImagesMutation.isPending

  // TODO add functions
}
```

The `CategoryMediaModal` component accepts the following props:

- `categoryId`: The ID of the category to manage images for.
- `existingImages`: The existing category images.

In the component, you define the following variables:

- `open`: A boolean indicating whether the modal is open.
- `uploadedFiles`: An array of newly uploaded files not yet created as category images.
- `currentThumbnailId`: The ID of the current thumbnail image.
- `fileInputRef`: A reference to the hidden file input element.
- `queryClient`: The Tanstack Query client for managing query caching and invalidation.
- `uploadFilesMutation` and `createImagesMutation`: The mutations returned by the `useCategoryImageMutations` hook.
- `isSaving`: A boolean indicating whether an operation, such as creating images, is in progress.

Next, you'll add functions to handle modal state changes. Replace the `// TODO add functions` comment with the following:

```tsx title="src/admin/components/category-media/category-media-modal.tsx" highlights={categoryMediaModalHighlights2}
const resetModalState = () => {
  setUploadedFiles([])
  setCurrentThumbnailId(null)
}

const initializeThumbnail = () => {
  const thumbnailImage = existingImages.find((img) => img.type === "thumbnail")
  if (thumbnailImage?.id) {
    setCurrentThumbnailId(thumbnailImage.id)
  }
}

const handleOpenChange = (isOpen: boolean) => {
  setOpen(isOpen)
  if (isOpen) {
    initializeThumbnail()
  } else {
    resetModalState()
  }
}

// TODO handle upload file
```

You add three functions:

1. `resetModalState`: Resets the modal's state by clearing uploaded files and the current thumbnail ID.
2. `initializeThumbnail`: Initializes the current thumbnail ID based on existing images when the modal opens.
3. `handleOpenChange`: Handles changes to the modal's open state, initializing or resetting the state as needed.

Next, you'll add a function to handle file uploads. Replace the `// TODO handle upload file` comment with the following:

```tsx title="src/admin/components/category-media/category-media-modal.tsx"
const handleUploadFile = (files: FileList | null) => {
  if (!files || files.length === 0) {return}
  const filesArray = Array.from(files)
  
  uploadFilesMutation.mutate(filesArray, {
    onSuccess: (data) => {
      setUploadedFiles((prev) => [...prev, ...data.files])
    },
  })
  
  if (fileInputRef.current) {
    fileInputRef.current.value = ""
  }
}

// TODO handle save
```

You add the `handleUploadFile` function, which is called when files are selected or dropped. It uploads the files using `uploadFilesMutation` and updates the `uploadedFiles` state with the uploaded files.

Next, you'll add a function to handle saving the uploaded files as category images. Replace the `// TODO handle save` comment with the following:

```tsx title="src/admin/components/category-media/category-media-modal.tsx"
const handleSave = async () => {
  const hasNewImages = uploadedFiles.length > 0

  try {
    const operations: Array<Promise<unknown>> = []
    if (hasNewImages) {
      const imagesToCreate = uploadedFiles.map((file) => ({
        url: file.url,
        file_id: file.id,
        type: file.type || (currentThumbnailId === `uploaded:${file.id}` ? 
          "thumbnail" : "image"
        ),
      }))
      operations.push(createImagesMutation.mutateAsync(imagesToCreate))
    }

    // TODO add update and delete operations

    await Promise.all(operations)

    queryClient.invalidateQueries({ queryKey: ["category-images", categoryId] })
    setOpen(false)
    resetModalState()
    toast.success("Category media saved successfully")
  } catch (error) {
    toast.error("Failed to save changes")
  }
}

// TODO render modal
```

You add the `handleSave` function, which is called when the user clicks the "Save" button in the modal. It creates category images for the uploaded files using `createImagesMutation`.

You'll revisit this function later to add update and delete operations.

Finally, you'll render the modal. Replace the `// TODO render modal` comment with the following:

```tsx title="src/admin/components/category-media/category-media-modal.tsx"
return (
  <FocusModal open={open} onOpenChange={handleOpenChange}>
    <FocusModal.Trigger asChild>
      <Button size="small" variant="secondary">
        Edit
      </Button>
    </FocusModal.Trigger>

    <FocusModal.Content>
      <FocusModal.Header>
        <Heading>Edit Media</Heading>
      </FocusModal.Header>

      <FocusModal.Body className="flex h-full overflow-hidden">
        <div className="flex w-full h-full flex-col-reverse lg:grid lg:grid-cols-[1fr_560px]">
          <CategoryImageGallery
            existingImages={existingImages}
            uploadedFiles={uploadedFiles}
            currentThumbnailId={currentThumbnailId}
          />
          <CategoryImageUpload
            fileInputRef={fileInputRef}
            isUploading={uploadFilesMutation.isPending}
            onFileSelect={handleUploadFile}
          />
        </div>
        {/* TODO show command bar */}
      </FocusModal.Body>
      <FocusModal.Footer>
        <div className="flex items-center justify-end gap-x-2">
          <FocusModal.Close asChild>
            <Button size="small" variant="secondary">
              Cancel
            </Button>
          </FocusModal.Close>
          <Button
            size="small"
            onClick={handleSave}
            isLoading={isSaving}
          >
            Save
          </Button>
        </div>
      </FocusModal.Footer>
    </FocusModal.Content>
  </FocusModal>
)
```

You render a modal using the `FocusModal` component from [Medusa UI](https://docs.medusajs.com/ui/index.html.md). The modal displays the `CategoryImageGallery` component on the left and the `CategoryImageUpload` component on the right.

You also render an "Edit" button that opens the modal when clicked.

### e. Add Modal to Widget

Finally, add the `CategoryMediaModal` component to the `CategoryMediaWidget` component.

In `src/admin/widgets/category-media-widget.tsx`, add the following import at the top:

```tsx title="src/admin/widgets/category-media-widget.tsx"
import { CategoryMediaModal } from "../components/category-media/category-media-modal"
```

Then, in the `CategoryMediaWidget`'s `return` statement, replace the `/* TODO show edit modal */` comment with the following:

```tsx title="src/admin/widgets/category-media-widget.tsx"
<CategoryMediaModal categoryId={data.id} existingImages={images} />
```

You add the `CategoryMediaModal` component, passing the category ID and existing images as props.

### Test the Media Widget

You can now test the media widget in the Medusa Admin dashboard.

Run the following command in the Medusa project directory to start the Medusa server:

```bash npm2yarn
npm run dev
```

Then, go to `localhost:9000/app` in your browser and:

1. Log in with the admin user you created earlier.
2. Go to Products → Categories.
3. Click on a category to view its details.

You'll see a new Media section in the category details page with an "Edit" button.

If you click the "Edit" button, a modal will open where you can upload new images.

Images are uploaded to the [configured File Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file#what-is-a-file-module-provider/index.html.md). If you haven't configured one, images will be uploaded to the `static` folder in your Medusa project.

![Media widget showing images with upload form](https://res.cloudinary.com/dza7lstvk/image/upload/v1760455846/Medusa%20Resources/CleanShot_2025-10-14_at_18.29.49_2x_vhbn6z.png)

After uploading images, you can click the "Save" button to create the category images. The images will be displayed in the Media section of the category details page.

![Media widget showing images after upload](https://res.cloudinary.com/dza7lstvk/image/upload/v1760456027/Medusa%20Resources/CleanShot_2025-10-14_at_18.33.27_2x_au7nxg.png)

***

## Step 6: Update Product Category Images

In this step, you'll implement the functionality to update a category image's type (between "thumbnail" and "image"). This includes:

- Creating a workflow that updates category images.
- Adding an API route that exposes the workflow's functionality.
- Updating the Medusa Admin modal to allow updating image types.

### a. Update Category Images Workflow

The workflow that updates category images has the following steps:

- [updateCategoryImagesStep](#updateCategoryImagesStep): Update category images

Medusa provides the `useQueryGraphStep`, and you've already created the `convertCategoryThumbnailsStep` in [step 3](#convertcategorythumbnailsstep). You only need to create the `updateCategoryImagesStep`.

#### updateCategoryImagesStep

The `updateCategoryImagesStep` updates the category images.

To create the step, create the file `src/workflows/steps/update-category-images.ts` with the following content:

```ts title="src/workflows/steps/update-category-images.ts" highlights={updateCategoryImagesStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PRODUCT_MEDIA_MODULE } from "../../modules/product-media"
import ProductMediaModuleService from "../../modules/product-media/service"

export type UpdateCategoryImagesStepInput = {
  updates: {
    id: string
    type?: "thumbnail" | "image"
  }[]
}

export const updateCategoryImagesStep = createStep(
  "update-category-images-step",
  async (input: UpdateCategoryImagesStepInput, { container }) => {
    const productMediaService: ProductMediaModuleService =
      container.resolve(PRODUCT_MEDIA_MODULE)

    // Get previous data for the images being updated
    const prevData = await productMediaService.listProductCategoryImages({
      id: input.updates.map((u) => u.id),
    })

    // Apply the requested updates
    const updatedData = await productMediaService.updateProductCategoryImages(
      input.updates
    )

    return new StepResponse(updatedData, prevData)
  },
  async (compensationData, { container }) => {
    if (!compensationData?.length) {
      return
    }

    const productMediaService: ProductMediaModuleService =
      container.resolve(PRODUCT_MEDIA_MODULE)

    // Revert all updates
    await productMediaService.updateProductCategoryImages(
      compensationData.map((img) => ({
        id: img.id,
        type: img.type,
      }))
    )
  }
)
```

This step accepts an array of updates, where each update contains the category image ID to update and the new type.

You update the category images in the step function and revert the updates in the compensation function.

#### Update Workflow

Next, you'll create the workflow that uses the step you just created to update category images.

Create the file `src/workflows/steps/update-category-images.ts` with the following content:

```ts title="src/workflows/update-category-images.ts" highlights={updateCategoryImagesWorkflowHighlights} collapsibleLines="1-10" expandButtonLabel="Show Imports"
import {
  createWorkflow,
  WorkflowResponse,
  transform,
  when,
} from "@medusajs/framework/workflows-sdk"
import { updateCategoryImagesStep } from "./steps/update-category-images"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { convertCategoryThumbnailsStep } from "./steps/convert-category-thumbnails"

export type UpdateCategoryImagesInput = {
  updates: {
    id: string
    type?: "thumbnail" | "image"
  }[]
}

export const updateCategoryImagesWorkflow = createWorkflow(
  "update-category-images",
  (input: UpdateCategoryImagesInput) => {
    when(input, (data) => data.updates.some((u) => u.type === "thumbnail"))
    .then(
      () => {
        const categoryImageIds = transform({
          input,
        }, (data) => data.input.updates.filter(
          (u) => u.type === "thumbnail"
        ).map((u) => u.id))
        const { data: categoryImages } = useQueryGraphStep({
          entity: "product_category_image",
          fields: ["category_id"],
          filters: {
            id: categoryImageIds,
          },
          options: {
            throwIfKeyNotFound: true,
          },
        })
        const categoryIds = transform({
          categoryImages,
        }, (data) => data.categoryImages.map((img) => img.category_id))
    
        convertCategoryThumbnailsStep({
          category_ids: categoryIds,
        })  
      }
    )
    const updatedImages = updateCategoryImagesStep({
      updates: input.updates,
    })

    return new WorkflowResponse(updatedImages)
  }
)
```

The workflow accepts the category images to update.

In the workflow, you:

1. Check if any of the updates set an image to be a thumbnail using a `when` condition.
   - If so, you retrieve the category IDs of the images being updated to thumbnails using the `useQueryGraphStep`, which uses Query to retrieve data across modules.
   - You then call the `convertCategoryThumbnailsStep` to convert any existing thumbnails in those categories to regular images.
2. Finally, you call the `updateCategoryImagesStep` to update the category images.

### b. Update Category Images API Route

Next, you'll create an API route that exposes the `updateCategoryImagesWorkflow`'s functionality.

Create the file `src/api/admin/categories/[category_id]/images/batch/route.ts` with the following content:

```ts title="src/api/admin/categories/[category_id]/images/batch/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import {
  updateCategoryImagesWorkflow,
} from "../../../../../../workflows/update-category-images"
import { z } from "zod"

export const UpdateCategoryImagesSchema = z.object({
  updates: z.array(z.object({
    id: z.string(),
    type: z.enum(["thumbnail", "image"]),
  })).min(1, "At least one update is required"),
})

type UpdateCategoryImagesInput = z.infer<typeof UpdateCategoryImagesSchema>

export async function POST(
  req: MedusaRequest<UpdateCategoryImagesInput>,
  res: MedusaResponse
): Promise<void> {
  const { updates } = req.validatedBody

  const { result } = await updateCategoryImagesWorkflow(req.scope).run({
    input: { updates },
  })

  res.status(200).json({ category_images: result })
}
```

You create a `POST` API route at `/admin/categories/:category_id/images/batch` that accepts an array of category images to update in the request body.

You validate the request body using a Zod schema, then execute the `updateCategoryImagesWorkflow` with the validated input.

Finally, you return the updated category images in the response.

### c. Add Validation Middleware

To ensure the request body is validated using the Zod schema you created, you'll add a validation middleware to the new API route.

In `src/api/middlewares.ts`, add the following import at the top of the file:

```ts title="src/api/middlewares.ts"
import { 
  UpdateCategoryImagesSchema,
} from "./admin/categories/[category_id]/images/batch/route"
```

Then, add a new route object passed to the array in `defineMiddlewares`:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/categories/:category_id/images/batch",
      method: ["POST"],
      middlewares: [
        validateAndTransformBody(UpdateCategoryImagesSchema),
      ],
    },
  ],
})
```

All `POST` requests to `/admin/categories/:category_id/images/batch` will now be validated using the `UpdateCategoryImagesSchema`.

### d. Add Update Mutation

Next, add a mutation to the `useCategoryImageMutations` hook for updating category images.

In `src/admin/hooks/use-category-image.ts`, update the `UseCategoryImageMutationsProps` type to include an `onUpdateSuccess` callback:

```ts title="src/admin/hooks/use-category-image.ts" highlights={[["3"]]}
type UseCategoryImageMutationsProps = {
  // ...
  onUpdateSuccess?: () => void
}
```

Then, in `useCategoryImageMutations`, add the `onUpdateSuccess` prop to the function parameters:

```ts title="src/admin/hooks/use-category-image.ts" highlights={[["3"]]}
export const useCategoryImageMutations = ({
  // ...
  onUpdateSuccess,
}: UseCategoryImageMutationsProps) => {
  // ...
}
```

Next, add the `updateImagesMutation` mutation inside the `useCategoryImageMutations` function, after the `createImagesMutation`:

```ts title="src/admin/hooks/use-category-image.ts"
const updateImagesMutation = useMutation({
  mutationFn: async (
    updates: { id: string; type: "thumbnail" | "image" }[]
  ) => {
    const response = await sdk.client.fetch(
      `/admin/categories/${categoryId}/images/batch`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: {
          updates,
        },
      }
    )
    return response
  },
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ["category-images", categoryId] })
    onUpdateSuccess?.()
  },
})
```

Finally, add `updateImagesMutation` to the returned object of the `useCategoryImageMutations` hook:

```ts title="src/admin/hooks/use-category-image.ts" highlights={[["3"]]}
return {
  // ...
  updateImagesMutation,
}
```

### e. Add Selection in Category Image Item

Next, add the ability to select a category image in the `CategoryImageItem` component. You'll use this selection to choose which image to set as the thumbnail, and later to delete images.

In `src/admin/components/category-media/category-image-item.tsx`, add the following imports at the top of the file:

```tsx title="src/admin/components/category-media/category-image-item.tsx"
import { Checkbox, clx } from "@medusajs/ui"
```

Then update the `CategoryImageItemProps` type to include two new props:

```tsx title="src/admin/components/category-media/category-image-item.tsx" highlights={[["3"], ["4"]]}
type CategoryImageItemProps = {
  // ...
  isSelected: boolean
  onToggleSelect: () => void
}
```

You add two new props:

- `isSelected`: A boolean indicating whether the image is selected.
- `onToggleSelect`: A callback function that is called when the selection state changes.

Next, update the props in the `CategoryImageItem` component:

```tsx title="src/admin/components/category-media/category-image-item.tsx" highlights={[["3"], ["4"]]}
export const CategoryImageItem = ({
  // ...
  isSelected,
  onToggleSelect,
}: CategoryImageItemProps) => {
  // ...
}
```

Finally, replace the `TODO` in the component's `return` statement with the following:

```tsx title="src/admin/components/category-media/category-image-item.tsx" 
<div className={clx(
  "transition-fg absolute right-2 top-2 opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 group-focus:opacity-100",
  isSelected && "opacity-100"
)}>
  <Checkbox
    checked={isSelected}
    onCheckedChange={onToggleSelect}
  />
</div>
```

You add a checkbox in the top-right corner of the image that indicates whether it's selected. The checkbox is visible when the image is hovered or selected.

When the checkbox state changes, it calls the `onToggleSelect` callback to update the selection state.

### f. Update Category Image Gallery

Next, you'll update the `CategoryImageGallery` component to manage the selection state of category images.

In `src/admin/components/category-media/category-image-gallery.tsx`, update the `CategoryImageGalleryProps` type to include two new props:

```tsx title="src/admin/components/category-media/category-image-gallery.tsx" highlights={[["3"], ["4"]]}
type CategoryImageGalleryProps = {
  // ...
  selectedImageIds: Set<string>
  onToggleSelect: (id: string, isUploaded?: boolean) => void
}
```

You add two new props:

- `selectedImageIds`: A set of IDs of the selected images.
- `onToggleSelect`: A callback function that is called when an image's selection state changes.

Then, update the props in the `CategoryImageGallery` component:

```tsx title="src/admin/components/category-media/category-image-gallery.tsx" highlights={[["3"], ["4"]]}
export const CategoryImageGallery = ({
  // ...
  selectedImageIds,
  onToggleSelect,
}: CategoryImageGalleryProps) => {
  // ...
}
```

Next, update the `CategoryImageItem` components in the `return` statement to pass the new props:

```tsx title="src/admin/components/category-media/category-image-gallery.tsx" highlights={[["11"], ["12"], ["24"], ["25"]]}
return (
  <div className="bg-ui-bg-subtle size-full overflow-auto">
    {/* ... */}
    {/* Existing images */}
    {visibleExistingImages.map((image) => {
      // ...
      
      return (
        <CategoryImageItem
          // ...
          isSelected={selectedImageIds.has(imageId)}
          onToggleSelect={() => onToggleSelect(imageId)}
        />
      )
    })}

    {/* Newly uploaded files */}
    {uploadedFiles.map((file) => {
      // ...
      
      return (
        <CategoryImageItem
          // ...
          isSelected={selectedImageIds.has(uploadedId)}
          onToggleSelect={() => onToggleSelect(file.id, true)}
        />
      )
    })}

    {/* ... */}
  </div>
)
```

You pass the `isSelected` prop to indicate whether the image is selected, and the `onToggleSelect` prop to handle selection changes.

### g. Update Category Media Modal

Lastly, you'll update the `CategoryMediaModal` component to manage the selection state and implement the update functionality.

In `src/admin/components/category-media/category-media-modal.tsx`, add the following import at the top of the file:

```tsx title="src/admin/components/category-media/category-media-modal.tsx"
import { CommandBar } from "@medusajs/ui"
```

You'll use the `CommandBar` component from Medusa UI to show actions like "Set as Thumbnail" and "Delete".

Then, in the `CategoryMediaModal` component, add a new state variable to manage the selected image IDs:

```tsx title="src/admin/components/category-media/category-media-modal.tsx"
const [selectedImageIds, setSelectedImageIds] = useState<Set<string>>(new Set())
```

Next, add to the destructured variables the `updateImagesMutation` from the `useCategoryImageMutations` hook, and pass the `onUpdateSuccess` callback:

```tsx title="src/admin/components/category-media/category-media-modal.tsx" highlights={[["3"], ["6"], ["7"], ["8"]]}
const {
  // ...
  updateImagesMutation,
} = useCategoryImageMutations({
  // ...
  onUpdateSuccess: () => {
    setSelectedImageIds(new Set())
  },
})
```

After that, update the `isSaving` variable to include the `updateImagesMutation`'s pending state:

```tsx title="src/admin/components/category-media/category-media-modal.tsx"
const isSaving = 
  createImagesMutation.isPending ||
  updateImagesMutation.isPending
```

Next, update the `resetModalState` function to clear the selected image IDs:

```tsx title="src/admin/components/category-media/category-media-modal.tsx" highlights={[["3"]]}
const resetModalState = () => {
  // ...
  setSelectedImageIds(new Set())
}
```

Next, add a function that toggles the selection state of an image:

```tsx title="src/admin/components/category-media/category-media-modal.tsx"
const handleImageSelection = (id: string, isUploaded: boolean = false) => {
  const itemId = isUploaded ? `uploaded:${id}` : id
  const newSelected = new Set(selectedImageIds)
  if (newSelected.has(itemId)) {
    newSelected.delete(itemId)
  } else {
    newSelected.add(itemId)
  }
  setSelectedImageIds(newSelected)
}
```

The `handleImageSelection` function accepts the image ID and a boolean indicating whether it's an uploaded file (not yet created as a category image).

It toggles the selection state of the image by adding or removing its ID from the `selectedImageIds` set.

Then, add a function that sets the selected image as the thumbnail:

```tsx title="src/admin/components/category-media/category-media-modal.tsx"
const handleSetAsThumbnail = () => {
  if (selectedImageIds.size !== 1) {return}

  const selectedId = Array.from(selectedImageIds)[0]
  setCurrentThumbnailId(selectedId)
  if (selectedId.startsWith("uploaded:")) {
    // update uploaded file type to thumbnail
    const uploadedFileId = selectedId.replace("uploaded:", "")
    setUploadedFiles((prev) =>
      prev.map((file) => {
        return file.id === uploadedFileId ? { ...file, type: "thumbnail" } : file
      })
    )
  }
  
  setSelectedImageIds(new Set())
}
```

The `handleSetAsThumbnail` function checks if exactly one image is selected. If so, it sets that image as the current thumbnail by updating the `currentThumbnailId` state.

If the selected image is an uploaded file (not yet created as a category image), it updates its type to "thumbnail" in the `uploadedFiles` state.

Next, update the `handleSave` function to include the update operation for changing image types:

```tsx title="src/admin/components/category-media/category-media-modal.tsx" highlights={handleSaveChangesHighlights1}
const handleSave = async () => {
  const hasNewImages = uploadedFiles.length > 0
  
  const initialThumbnail = existingImages.find(
    (img) => img.type === "thumbnail"
  )
  const thumbnailChanged = 
    currentThumbnailId && 
    !currentThumbnailId.startsWith("uploaded:") &&
    currentThumbnailId !== initialThumbnail?.id

  if (!hasNewImages && !thumbnailChanged) {
    setOpen(false)
    return
  }

  try {
    const operations: Array<Promise<unknown>> = []
    if (hasNewImages) {
      const imagesToCreate = uploadedFiles.map((file) => ({
        url: file.url,
        file_id: file.id,
        type: file.type || (currentThumbnailId === `uploaded:${file.id}` ? 
          "thumbnail" : "image"
        ),
      }))
      operations.push(createImagesMutation.mutateAsync(imagesToCreate))
    }

    // Update thumbnail if changed
    if (thumbnailChanged) {
      const updates = [
        {
          id: currentThumbnailId,
          type: "thumbnail" as const,
        },
      ]
      operations.push(updateImagesMutation.mutateAsync(updates))
    }

    await Promise.all(operations)

    queryClient.invalidateQueries({ queryKey: ["category-images", categoryId] })
    setOpen(false)
    resetModalState()
    toast.success("Category media saved successfully")
  } catch (error) {
    toast.error("Failed to save changes")
  }
}
```

You update the `handleSave` function to:

- Check if the thumbnail has changed and isn't an uploaded file.
- If the thumbnail has changed, add an update operation to the `operations` array to set the image type to "thumbnail" using the `updateImagesMutation`.
- Ensure that if the new thumbnail is an uploaded file, it doesn't attempt to update it, since it will be created with the correct type.

Finally, in the `return` statement, replace the `/* TODO show command bar */` comment with the following:

```tsx title="src/admin/components/category-media/category-media-modal.tsx"
<CommandBar open={selectedImageIds.size > 0}>
  <CommandBar.Bar>
    <CommandBar.Value>
      {selectedImageIds.size} selected
    </CommandBar.Value>
    <CommandBar.Seperator />
    <CommandBar.Command
      action={handleSetAsThumbnail}
      label="Set as thumbnail"
      shortcut="t"
      disabled={selectedImageIds.size !== 1}
    />
    {/* TODO add delete command */}
  </CommandBar.Bar>
</CommandBar>
```

You add a `CommandBar` that shows the number of selected images and a command to "Set as thumbnail". The command is disabled unless exactly one image is selected.

Then, update the `CategoryImageGallery` component to pass the new props:

```tsx title="src/admin/components/category-media/category-media-modal.tsx" highlights={[["3"], ["4"]]}
<CategoryImageGallery
  // ...
  selectedImageIds={selectedImageIds}
  onToggleSelect={handleImageSelection}
/>
```

You pass the `selectedImageIds` state and the `handleImageSelection` function to manage image selection.

### Test Update Functionality

You can now test the update functionality in the Medusa Admin dashboard.

Start the Medusa server if it's not already running, and go to a category's details page:

1. Click the "Edit" button in the Media section to open the modal.
2. Hover over an image and click the checkbox to select it.
3. You'll see a command bar at the bottom, where you can click "Set as thumbnail" to set the selected image as the thumbnail. You can also press the "t" key as a shortcut.
4. Click the "Save" button to save the changes.

![Media widget showing command bar with set as thumbnail action](https://res.cloudinary.com/dza7lstvk/image/upload/v1760514879/Medusa%20Resources/CleanShot_2025-10-15_at_10.54.18_2x_qfrwgy.png)

You'll now see the thumbnail icon on the image in the Media section of the category details page.

![Media widget showing updated thumbnail](https://res.cloudinary.com/dza7lstvk/image/upload/v1760515015/Medusa%20Resources/CleanShot_2025-10-15_at_10.56.24_2x_suv0qr.png)

***

## Step 7: Delete Product Category Images

In this step, you'll implement the functionality to delete category images. This includes:

1. Creating a workflow that deletes category images.
2. Adding an API route that exposes the workflow's functionality.
3. Updating the Medusa Admin modal to allow deleting images.

### a. Delete Category Images Workflow

The workflow that deletes category images has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the file IDs of the images
- [deleteFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFilesWorkflow/index.html.md): Delete the files from storage.
- [deleteCategoryImagesStep](#deleteCategoryImagesStep): Delete the category images.

The first two steps are available out-of-the-box in Medusa. You only need to create the last step.

#### deleteCategoryImagesStep

The `deleteCategoryImagesStep` step deletes the category images.

To create the step, create the file `src/workflows/steps/delete-category-image.ts` with the following content:

```ts title="src/workflows/steps/delete-category-images.ts" highlights={deleteCategoryImagesStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PRODUCT_MEDIA_MODULE } from "../../modules/product-media"
import ProductMediaModuleService from "../../modules/product-media/service"

export type DeleteCategoryImagesStepInput = {
  ids: string[]
}

export const deleteCategoryImagesStep = createStep(
  "delete-category-images-step",
  async (input: DeleteCategoryImagesStepInput, { container }) => {
    const productMediaService: ProductMediaModuleService =
      container.resolve(PRODUCT_MEDIA_MODULE)

    // Retrieve the full category images data before deleting
    const categoryImages = await productMediaService.listProductCategoryImages({
      id: input.ids,
    })

    // Delete the category images
    await productMediaService.deleteProductCategoryImages(input.ids)

    return new StepResponse(
      { success: true, deleted: input.ids }, 
      categoryImages
    )
  },
  async (categoryImages, { container }) => {
    if (!categoryImages || categoryImages.length === 0) {
      return
    }

    const productMediaService: ProductMediaModuleService =
      container.resolve(PRODUCT_MEDIA_MODULE)

    // Recreate all category images with their original data
    await productMediaService.createProductCategoryImages(
      categoryImages.map((categoryImage) => ({
        id: categoryImage.id,
        category_id: categoryImage.category_id,
        type: categoryImage.type,
        url: categoryImage.url,
        file_id: categoryImage.file_id,
      }))
    )
  }
)
```

This step accepts an array of category image IDs to delete.

In the step, you first retrieve the full data of the category images to be deleted. This is necessary for the compensation function to recreate them.

Then, you delete the category images and pass the deleted data to the compensation function.

In the compensation function, you recreate the deleted category images using their original data if an error occurs during workflow execution.

#### Delete Workflow

Next, you'll create the workflow that uses the step you just created to delete category images.

Create the file `src/workflows/delete-category-image.ts` with the following content:

```ts title="src/workflows/delete-category-images.ts" highlights={deleteCategoryImagesWorkflowHighlights} collapsibleLines="1-10" expandButtonLabel="Show Imports"
import {
  createWorkflow,
  WorkflowResponse,
  transform,
} from "@medusajs/framework/workflows-sdk"
import { deleteFilesWorkflow, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { deleteCategoryImagesStep } from "./steps/delete-category-image"

export type DeleteCategoryImagesInput = {
  ids: string[]
}

export const deleteCategoryImagesWorkflow = createWorkflow(
  "delete-category-images",
  (input: DeleteCategoryImagesInput) => {
    // First, get the category images to retrieve the file_ids
    const { data: categoryImages } = useQueryGraphStep({
      entity: "product_category_image",
      fields: ["id", "file_id", "url", "type", "category_id"],
      filters: {
        id: input.ids,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    // Transform the category images to extract file IDs
    const fileIds = transform(
      { categoryImages },
      (data) => data.categoryImages.map((img) => img.file_id)
    )

    // Delete the files from storage
    deleteFilesWorkflow.runAsStep({
      input: {
        ids: fileIds,
      },
    })

    // Then delete the category image records
    const result = deleteCategoryImagesStep({ ids: input.ids })

    return new WorkflowResponse(result)
  }
)
```

The workflow accepts the IDs of the category images to delete.

In the workflow, you:

1. Retrieve the category images using `useQueryGraphStep` to get their file IDs. This step uses Query to retrieve data across modules.
2. Prepare the file IDs using [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md).
3. Delete the files from storage using `deleteFilesWorkflow`.
4. Delete the category images using `deleteCategoryImagesStep`.

### b. Delete Category Images API Route

Next, you'll create an API route that exposes the `deleteCategoryImagesWorkflow`'s functionality.

In `src/api/admin/categories/[category_id]/images/batch/route.ts`, add the following import at the top of the file:

```ts title="src/api/admin/categories/[category_id]/images/batch/route.ts"
import {
  deleteCategoryImagesWorkflow,
} from "../../../../../../workflows/delete-category-image"
```

Then, add the following at the end of the file:

```ts title="src/api/admin/categories/[category_id]/images/batch/route.ts"
export const DeleteCategoryImagesSchema = z.object({
  ids: z.array(z.string()).min(1, "At least one ID is required"),
})

type DeleteCategoryImagesInput = z.infer<typeof DeleteCategoryImagesSchema>

export async function DELETE(
  req: MedusaRequest<DeleteCategoryImagesInput>,
  res: MedusaResponse
): Promise<void> {
  const { ids } = req.validatedBody

  await deleteCategoryImagesWorkflow(req.scope).run({
    input: { ids },
  })

  res.status(200).json({
    deleted: ids,
  })
}
```

You create a `DELETE` API route at `/admin/categories/:category_id/images/batch` that accepts an array of category image IDs to delete in the request body.

You validate the request body using a Zod schema, then execute the `deleteCategoryImagesWorkflow` with the validated input.

Finally, you return the deleted category image IDs in the response.

### c. Add Validation Middleware

To ensure the request body is validated using the Zod schema you created, you'll add a validation middleware to the new API route.

In `src/api/middlewares.ts`, add the following import at the top of the file:

```ts title="src/api/middlewares.ts"
import { 
  DeleteCategoryImagesSchema,
} from "./admin/categories/[category_id]/images/batch/route"
```

Then, add a new route object passed to the array in `defineMiddlewares`:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/categories/:category_id/images/batch",
      method: ["DELETE"],
      middlewares: [
        validateAndTransformBody(DeleteCategoryImagesSchema),
      ],
    },
  ],
})
```

All `DELETE` requests to `/admin/categories/:category_id/images/batch` will now be validated using the `DeleteCategoryImagesSchema`.

### c. Add Delete Mutation

Next, you'll add a mutation to the `useCategoryImageMutations` hook to delete category images.

In `src/admin/hooks/use-category-image.ts`, update the `UseCategoryImageMutationsProps` type to include an `onDeleteSuccess` callback:

```ts title="src/admin/hooks/use-category-image.ts" highlights={[["3"]]}
type UseCategoryImageMutationsProps = {
  // ...
  onDeleteSuccess?: (deletedIds: string[]) => void
}
```

Then, in `useCategoryImageMutations`, add the `onDeleteSuccess` prop to the function parameters:

```ts title="src/admin/hooks/use-category-image.ts" highlights={[["3"]]}
export const useCategoryImageMutations = ({
  // ...
  onDeleteSuccess,
}: UseCategoryImageMutationsProps) => {
  // ...
}
```

Next, add the `deleteImagesMutation` mutation inside the `useCategoryImageMutations` function, after the `updateImagesMutation`:

```ts title="src/admin/hooks/use-category-image.ts"
const deleteImagesMutation = useMutation({
  mutationFn: async (ids: string[]) => {
    const response = await sdk.client.fetch(
      `/admin/categories/${categoryId}/images/batch`,
      {
        method: "DELETE",
        headers: {
          "Content-Type": "application/json",
        },
        body: {
          ids,
        },
      }
    )
    return response
  },
  onSuccess: (_data, deletedIds) => {
    queryClient.invalidateQueries({ queryKey: ["category-images", categoryId] })
    onDeleteSuccess?.(deletedIds)
  },
})
```

Finally, add `deleteImagesMutation` to the returned object of the `useCategoryImageMutations` hook:

```ts title="src/admin/hooks/use-category-image.ts" highlights={[["3"]]}
return {
  // ...
  deleteImagesMutation,
}
```

### d. Update Category Image Gallery

Next, you'll update the `CategoryImageGallery` component to hide images to be deleted.

In `src/admin/components/category-media/category-image-gallery.tsx`, update the `CategoryImageGalleryProps` type to include a new prop:

```tsx title="src/admin/components/category-media/category-image-gallery.tsx" highlights={[["3"]]}
type CategoryImageGalleryProps = {
  // ...
  imagesToDelete: Set<string>
}
```

You add the `imagesToDelete` prop, which is a set of IDs of the images to be deleted.

Then, update the props in the `CategoryImageGallery` component:

```tsx title="src/admin/components/category-media/category-image-gallery.tsx" highlights={[["3"]]}
export const CategoryImageGallery = ({
  // ...
  imagesToDelete,
}: CategoryImageGalleryProps) => {
  // ...
}
```

Finally, update the `visibleExistingImages` to filter out images that are marked for deletion:

```tsx title="src/admin/components/category-media/category-image-gallery.tsx"
const visibleExistingImages = existingImages.filter(
  (image) => image.id && !imagesToDelete.has(image.id)
)
```

### e. Update Category Media Modal

Lastly, you'll update the `CategoryMediaModal` component to manage the images to be deleted and implement the delete functionality.

In `src/admin/components/category-media/category-media-modal.tsx`, add a new state variable to manage the IDs of images to be deleted:

```tsx title="src/admin/components/category-media/category-media-modal.tsx"
const [imagesToDelete, setImagesToDelete] = useState<Set<string>>(new Set())
```

Next, add to the destructured variables the `deleteImagesMutation` from the `useCategoryImageMutations` hook, and pass the `onDeleteSuccess` callback:

```tsx title="src/admin/components/category-media/category-media-modal.tsx" highlights={[["3"], ["6"], ["7"], ["8"], ["9"], ["10"], ["11"]]}
const {
  // ...
  deleteImagesMutation,
} = useCategoryImageMutations({
  // ...
  onDeleteSuccess: (deletedIds) => {
    setSelectedImageIds(new Set())
    if (currentThumbnailId && deletedIds.includes(currentThumbnailId)) {
      setCurrentThumbnailId(null)
    }
  },
})
```

You update the `onDeleteSuccess` callback to clear the selected image IDs and reset the current thumbnail if it was deleted.

Then, update the `isSaving` variable to include the `deleteImagesMutation`'s pending state:

```tsx title="src/admin/components/category-media/category-media-modal.tsx"
const isSaving = 
  createImagesMutation.isPending ||
  updateImagesMutation.isPending ||
  deleteImagesMutation.isPending
```

Next, update the `resetModalState` function to clear the images to be deleted:

```tsx title="src/admin/components/category-media/category-media-modal.tsx" highlights={[["3"]]}
const resetModalState = () => {
  // ...
  setImagesToDelete(new Set())
}
```

After that, add a function that marks selected images for deletion:

```tsx title="src/admin/components/category-media/category-media-modal.tsx"
const handleDelete = () => {
  if (selectedImageIds.size === 0) {return}

  const uploadedFileIds: string[] = []
  const savedImageIds: string[] = []

  selectedImageIds.forEach((id) => {
    if (id.startsWith("uploaded:")) {
      uploadedFileIds.push(id.replace("uploaded:", ""))
    } else {
      savedImageIds.push(id)
    }
  })

  if (uploadedFileIds.length > 0) {
    setUploadedFiles((prev) =>
      prev.filter((file) => !uploadedFileIds.includes(file.id))
    )
    if (currentThumbnailId?.startsWith("uploaded:")) {
      const thumbnailFileId = currentThumbnailId.replace("uploaded:", "")
      if (uploadedFileIds.includes(thumbnailFileId)) {
        setCurrentThumbnailId(null)
      }
    }
  }

  if (savedImageIds.length > 0) {
    setImagesToDelete((prev) => {
      const newSet = new Set(prev)
      savedImageIds.forEach((id) => newSet.add(id))
      return newSet
    })
    if (currentThumbnailId && savedImageIds.includes(currentThumbnailId)) {
      setCurrentThumbnailId(null)
    }
  }

  setSelectedImageIds(new Set())
}
```

In the `handleDelete` function, you:

- Check if any images are selected; if none, return early.
- Separate the selected IDs into `uploadedFileIds` (newly uploaded files) and `savedImageIds` (existing category images).
- For uploaded files, remove them from the `uploadedFiles` state. If the current thumbnail is among the deleted uploaded files, reset the thumbnail state.
- For saved images, add their IDs to the `imagesToDelete` state. If the current thumbnail is among the deleted saved images, reset the thumbnail state.
- Finally, clear the selected image IDs.

Next, update the `handleSave` function to include the delete operation:

```tsx title="src/admin/components/category-media/category-media-modal.tsx"
const handleSave = async () => {
  const hasNewImages = uploadedFiles.length > 0
  const hasImagesToDelete = imagesToDelete.size > 0
  
  const initialThumbnail = existingImages.find((img) => img.type === "thumbnail")
  const thumbnailChanged = 
    currentThumbnailId && 
    !currentThumbnailId.startsWith("uploaded:") &&
    currentThumbnailId !== initialThumbnail?.id

  if (!hasNewImages && !hasImagesToDelete && !thumbnailChanged) {
    setOpen(false)
    return
  }

  try {
    const operations: Array<Promise<unknown>> = []
    if (hasNewImages) {
      const imagesToCreate = uploadedFiles.map((file) => ({
        url: file.url,
        file_id: file.id,
        type: file.type || (currentThumbnailId === `uploaded:${file.id}` ? 
          "thumbnail" : "image"
        ),
      }))
      operations.push(createImagesMutation.mutateAsync(imagesToCreate))
    }

    // Update thumbnail if changed and it's not an uploaded file
    if (thumbnailChanged && !(hasNewImages && currentThumbnailId?.startsWith("uploaded:"))) {
      const updates = [
        {
          id: currentThumbnailId,
          type: "thumbnail" as const,
        },
      ]
      operations.push(updateImagesMutation.mutateAsync(updates))
    }

    if (hasImagesToDelete) {
      const idsToDelete = Array.from(imagesToDelete)
      operations.push(deleteImagesMutation.mutateAsync(idsToDelete))
    }

    await Promise.all(operations)

    queryClient.invalidateQueries({ queryKey: ["category-images", categoryId] })
    setOpen(false)
    resetModalState()
    toast.success("Category media saved successfully")
  } catch (error) {
    toast.error("Failed to save changes")
  }
}
```

You update the `handleSave` function to:

- Check if there are images to delete.
- If images need deletion, add a delete operation to the `operations` array using `deleteImagesMutation`.

Finally, in the `return` statement, replace the `/* TODO add delete command */` comment with the following:

```tsx title="src/admin/components/category-media/category-media-modal.tsx"
return (
  <CommandBar open={selectedImageIds.size > 0}>
    {/* ... */}
    <CommandBar.Seperator />
    <CommandBar.Command
      action={handleDelete}
      label="Delete"
      shortcut="d"
    />
  </CommandBar>
)
```

You add a command to "Delete" the selected images. You can also press the "d" key as a shortcut.

Then, update the `CategoryImageGallery` component to pass the new prop:

```tsx title="src/admin/components/category-media/category-media-modal.tsx" highlights={[["3"]]}
<CategoryImageGallery
  // ...
  imagesToDelete={imagesToDelete}
/>
```

You pass the `imagesToDelete` state to hide images that are marked for deletion.

### Test Delete Functionality

You can now test the delete functionality in the Medusa Admin dashboard.

Start the Medusa server if it's not already running, and go to a category's details page:

1. Click the "Edit" button in the Media section to open the modal.
2. Hover over an image and click the checkbox to select it.
3. You'll see a command bar at the bottom, where you can click "Delete" to mark the selected images for deletion. You can also press the "d" key as a shortcut.
4. Click the "Save" button to save the changes.

![Media widget showing command bar with delete action](https://res.cloudinary.com/dza7lstvk/image/upload/v1760516694/Medusa%20Resources/CleanShot_2025-10-15_at_11.23.42_2x_k1folo.png)

You'll see the selected images are removed from the Media section of the category details page.

![Media widget showing updated images after deletion](https://res.cloudinary.com/dza7lstvk/image/upload/v1760516693/Medusa%20Resources/CleanShot_2025-10-15_at_11.24.30_2x_rmgtqn.png)

***

## Step 8: Show Category Images in Storefront

In the last step, you'll update the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to:

- Add a megamenu that displays categories with their thumbnails.
- Display a banner image on category pages.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-category-images`, you can find the storefront by going back to the parent directory and changing to the `medusa-category-images-storefront` directory:

```bash
cd ../medusa-category-images-storefront # change based on your project name
```

### a. Add Read-Only Link

Before customizing the storefront, you need a way to retrieve a category's images from the Medusa backend.

You can do this by creating a [read-only link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/read-only/index.html.md). A read-only link allows you to retrieve data related to a model from another module without compromising [module isolation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

You'll create an inverse read-only link from the `ProductCategory` model in the `Product` module to the `ProductCategoryImage` model in the `ProductMedia` module.

To create the link, create the file `src/links/product-category-image.ts` with the following content:

```ts title="src/links/product-category-image.ts" badgeLabel="Medusa Application" badgeColor="green"
import { defineLink } from "@medusajs/framework/utils"
import ProductModule from "@medusajs/medusa/product"
import ProductMediaModule from "../modules/product-media"

export default defineLink(
  {
    linkable: ProductModule.linkable.productCategory,
    field: "id",
    isList: true,
  },
  {
    ...ProductMediaModule.linkable.productCategoryImage.id,
    primaryKey: "category_id",
  },
  {
    readOnly: true,
  }
)
```

You define a link using the `defineLink` function. It accepts three parameters:

1. An object indicating the first data model in the link. It has the following properties:
   - `linkable`: A module has a special `linkable` property containing link configurations for its data models. You pass the linkable configurations of the `ProductCategory` model.
   - `field`: The field in the `ProductCategory` model used to link to the `ProductCategoryImage` model. In this case, it's the `id` field.
   - `isList`: A boolean indicating whether the data model links to multiple records in the other data model. Since a category can have multiple images, you set it to `true`.
2. An object indicating the second data model in the link. It has the following properties:
   - You spread the linkable configurations of the `ProductCategoryImage` model.
   - `primaryKey`: The field in the `ProductCategoryImage` model that links back to the `ProductCategory` model. In this case, it's the `category_id` field.
3. An options object. You set the `readOnly` property to `true` to indicate this is a read-only link.

You'll learn how this link allows you to retrieve category images in the next section.

### b. Retrieve Category Images

You'll now begin customizing the storefront.

First, update the functions that retrieve categories to include their images.

In `src/lib/data/categories.ts`, update the `fields` query parameter in the `listCategories` and `getCategoryByHandle` functions to include the new link you created:

```ts title="src/lib/data/categories.ts" badgeLabel="Storefront" badgeColor="blue" highlights={[["9"], ["26"]]}
export const listCategories = async (query?: Record<string, any>) => {
  // ...
  return sdk.client
    .fetch<{ product_categories: HttpTypes.StoreProductCategory[] }>(
      "/store/product-categories",
      {
        query: {
          fields:
            "*category_children, *products, *parent_category, *parent_category.parent_category, *product_category_image",
          // ...
        },
        // ...
      }
    )
    // ...
}

export const getCategoryByHandle = async (categoryHandle: string[]) => {
  // ...

  return sdk.client
    .fetch<HttpTypes.StoreProductCategoryListResponse>(
      `/store/product-categories`,
      {
        query: {
          fields: "*category_children, *products, *product_category_image",
          // ...
        },
        // ...
      }
    )
    // ...
}
```

You add `*product_category_image` to the `fields` query parameter in both functions. The asterisk (`*`) indicates that you want to include all fields in the product category image record.

### c. Add Category Image Type

Next, you'll add a TypeScript type for a category image.

In `src/types/global.ts`, add the following type:

```ts title="src/types/global.ts" badgeLabel="Storefront" badgeColor="blue"
export type CategoryImage = {
  id?: string
  url: string
  type: "thumbnail" | "image"
  category_id?: string
}
```

You define a `CategoryImage` type that represents a category image.

### d. Add Megamenu

Next, you'll add a megamenu that shows categories with their thumbnail. You'll then change the navigation bar to show the megamenu.

#### Create Megamenu Component

To create the megamenu component, create the file `src/modules/layout/components/megamenu/index.tsx` with the following content:

```tsx title="src/modules/layout/components/megamenu/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { HttpTypes } from "@medusajs/types"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import { CategoryImage } from "../../../../types/global"
import Thumbnail from "../../../products/components/thumbnail"

type CategoryWithImages = HttpTypes.StoreProductCategory & {
  product_category_image?: CategoryImage[]
}

const Megamenu = ({
  categories,
}: {
  categories: CategoryWithImages[]
}) => {
  // Filter to only show parent categories (no parent_category_id)
  const parentCategories = categories.filter(
    (category) => !category.parent_category_id
  )

  return (
    <div className="h-full w-full hidden small:flex items-center justify-center">
      <div className="w-fit group/megamenu h-full">
        <LocalizedClientLink
          data-testid="nav-categories-button"
          className="relative h-full flex items-center focus:outline-none hover:text-ui-fg-base"
          href="/store"
        >
          Shop
        </LocalizedClientLink>

        {/* Megamenu dropdown */}
        <div className="absolute left-0 right-0 top-full z-30 opacity-0 invisible translate-y-1 group-hover/megamenu:opacity-100 group-hover/megamenu:visible group-hover/megamenu:translate-y-0 transition-all duration-150 ease-out w-full">
          <div className="bg-white border-b border-ui-border-base shadow-sm">
            <div className="content-container">
              <div
                data-testid="nav-categories-popup"
                className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-x-8 gap-y-6 py-8"
              >
                {parentCategories.map((category) => {
                  const thumbnail = category.product_category_image?.find(
                    (img) => img.type === "thumbnail"
                  )

                  return (
                    <LocalizedClientLink
                      key={category.id}
                      href={`/categories/${category.handle}`}
                      className="group/megamenu-item flex flex-col gap-2 focus:outline-none"
                      data-testid={`category-${category.handle}`}
                    >
                      <Thumbnail
                        thumbnail={thumbnail?.url}
                        size="square"
                        className="!shadow-none"
                      />
                      <div className="text-center">
                        <h3 className="text-xs text-ui-fg-base group-hover/megamenu-item:text-ui-fg-subtle transition-colors">
                          {category.name}
                        </h3>
                      </div>
                    </LocalizedClientLink>
                  )
                })}
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}

export default Megamenu
```

The `Megamenu` component accepts an array of categories with their images.

It filters the categories to show only parent categories (those without a `parent_category_id`).

Then, it renders a megamenu that displays each parent category with its thumbnail image and name. Each category links to its category page.

#### Update Navigation Bar

Next, you'll update the navigation bar to show the megamenu.

In `src/modules/layout/templates/nav/index.tsx`, update the file content to the following:

```tsx title="src/modules/layout/templates/nav/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { Suspense } from "react"
import { listCategories } from "@lib/data/categories"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import CartButton from "@modules/layout/components/cart-button"
import Megamenu from "@modules/layout/components/megamenu"

export default async function Nav() {
  const categories = await listCategories({
    limit: 5,
  })

  return (
    <div className="sticky top-0 inset-x-0 z-50 group">
      <header className="relative h-16 mx-auto border-b duration-200 bg-white border-ui-border-base">
        <nav className="content-container txt-xsmall-plus text-ui-fg-subtle flex items-center w-full h-full text-small-regular">
          {/* Left: Logo */}
          <div className="flex items-center gap-x-6 h-full">
            <LocalizedClientLink
              href="/"
              className="txt-compact-xlarge-plus hover:text-ui-fg-base uppercase"
              data-testid="nav-store-link"
            >
              Medusa Store
            </LocalizedClientLink>
          </div>

          {/* Center: Megamenu */}
          <div className="flex-1 flex justify-center h-full">
            <Megamenu categories={categories} />
          </div>

          {/* Right: Account and Cart */}
          <div className="flex items-center gap-x-6 h-full">
            <div className="hidden small:flex items-center gap-x-6 h-full">
              <LocalizedClientLink
                className="hover:text-ui-fg-base"
                href="/account"
                data-testid="nav-account-link"
              >
                Account
              </LocalizedClientLink>
            </div>
            <Suspense
              fallback={
                <LocalizedClientLink
                  className="hover:text-ui-fg-base flex gap-2"
                  href="/cart"
                  data-testid="nav-cart-link"
                >
                  Cart (0)
                </LocalizedClientLink>
              }
            >
              <CartButton />
            </Suspense>
          </div>
        </nav>
      </header>
    </div>
  )
}
```

You make the following key changes:

1. Retrieve the categories using the `listCategories` function, limiting it to 5 categories.
2. Move the logo to the left side of the navigation bar and remove the previous Menu item.
3. Add the `Megamenu` component in the center of the navigation bar, passing the retrieved categories as a prop.

#### Test Megamenu

To test out the megamenu, start the Medusa application with the following command:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

Then, run the following command in the Next.js Starter Storefront directory to start the storefront:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

Open the storefront at `http://localhost:8000` in your browser. You'll see the "Shop" item in the navigation bar.

Hover over the "Shop" item to see the megamenu with categories and their thumbnails.

![Storefront showing megamenu with categories and their thumbnails](https://res.cloudinary.com/dza7lstvk/image/upload/v1760518332/Medusa%20Resources/CleanShot_2025-10-15_at_11.51.56_2x_mifvqx.png)

### e. Show Banner Image on Category Page

Next, you'll show a banner image on a category's page.

#### Create Banner Component

To create the banner component, create the file `src/modules/categories/components/category-banner/index.tsx` with the following content:

```tsx title="src/modules/categories/components/category-banner/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import Image from "next/image"
import { CategoryImage } from ".././../../../types/global"

type CategoryBannerProps = {
  images?: CategoryImage[]
  categoryName: string
}

export default function CategoryBanner({
  images,
  categoryName,
}: CategoryBannerProps) {
  // Get the first image that is not a thumbnail
  const bannerImage = images?.find((img) => img.type === "image")

  if (!bannerImage) {
    return null
  }

  return (
    <div className="relative w-full h-64 md:h-80 lg:h-96 mb-8 overflow-hidden">
      <Image
        src={bannerImage.url}
        alt={categoryName}
        fill
        className="object-cover"
        priority
        sizes="100vw"
      />
    </div>
  )
}
```

The `CategoryBanner` component accepts an array of category images and the category name as props.

It retrieves the first non-thumbnail image and displays it as a banner. If no such image exists, it returns `null`.

#### Update Category Page

Next, you'll update the category page to include the banner component.

Replace the content of `src/modules/categories/templates/index.tsx` with the following:

```tsx title="src/modules/categories/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { notFound } from "next/navigation"
import { Suspense } from "react"

import InteractiveLink from "@modules/common/components/interactive-link"
import SkeletonProductGrid from "@modules/skeletons/templates/skeleton-product-grid"
import RefinementList from "@modules/store/components/refinement-list"
import { SortOptions } from "@modules/store/components/refinement-list/sort-products"
import PaginatedProducts from "@modules/store/templates/paginated-products"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import CategoryBanner from "@modules/categories/components/category-banner"
import { HttpTypes } from "@medusajs/types"
import { CategoryImage } from ".././../../types/global"

type CategoryWithImages = HttpTypes.StoreProductCategory & {
  product_category_image?: CategoryImage[]
}

export default function CategoryTemplate({
  category,
  sortBy,
  page,
  countryCode,
}: {
  category: CategoryWithImages
  sortBy?: SortOptions
  page?: string
  countryCode: string
}) {
  const pageNumber = page ? parseInt(page) : 1
  const sort = sortBy || "created_at"

  if (!category || !countryCode) {notFound()}

  const parents = [] as HttpTypes.StoreProductCategory[]

  const getParents = (category: HttpTypes.StoreProductCategory) => {
    if (category.parent_category) {
      parents.push(category.parent_category)
      getParents(category.parent_category)
    }
  }

  getParents(category)

  return (
    <>
      {/* Full-width banner outside content-container */}
      <CategoryBanner
        images={category.product_category_image}
        categoryName={category.name}
      />

      <div
        className="flex flex-col small:flex-row small:items-start pb-6 content-container"
        data-testid="category-container"
      >
        <RefinementList sortBy={sort} data-testid="sort-by-container" />
        <div className="w-full">
          <div className="flex flex-row mb-8 text-2xl-semi gap-4">
            {parents &&
              parents.map((parent) => (
                <span key={parent.id} className="text-ui-fg-subtle">
                  <LocalizedClientLink
                    className="mr-4 hover:text-black"
                    href={`/categories/${parent.handle}`}
                    data-testid="sort-by-link"
                  >
                    {parent.name}
                  </LocalizedClientLink>
                  /
                </span>
              ))}
            <h1 data-testid="category-page-title">{category.name}</h1>
          </div>
          {category.description && (
            <div className="mb-8 text-base-regular">
              <p>{category.description}</p>
            </div>
          )}
          {category.category_children && (
            <div className="mb-8 text-base-large">
              <ul className="grid grid-cols-1 gap-2">
                {category.category_children?.map((c) => (
                  <li key={c.id}>
                    <InteractiveLink href={`/categories/${c.handle}`}>
                      {c.name}
                    </InteractiveLink>
                  </li>
                ))}
              </ul>
            </div>
          )}
          <Suspense
            fallback={
              <SkeletonProductGrid
                numberOfProducts={category.products?.length ?? 8}
              />
            }
          >
            <PaginatedProducts
              sortBy={sort}
              page={pageNumber}
              categoryId={category.id}
              countryCode={countryCode}
            />
          </Suspense>
        </div>
      </div>
    </>
  )
}
```

You make the following key changes:

- Update the type of the `category` prop to include category images.
- Display the `CategoryBanner` component at the top of the page, passing the category images and name as props.

#### Test Category Banner

To test out the category banner, ensure both the Medusa application and the Next.js Starter Storefront are running.

Then, open the storefront at `http://localhost:8000` in your browser. You can navigate to a category page by clicking on a category in the megamenu.

You'll see the banner image at the top of the category page. If you don't see a banner image, ensure that the category has an image of type "image" (not "thumbnail") in the Medusa Admin dashboard.

![Storefront showing category page with banner image](https://res.cloudinary.com/dza7lstvk/image/upload/v1760518495/Medusa%20Resources/CleanShot_2025-10-15_at_11.54.44_2x_muadjw.png)

***

## Next Steps

You've now added support for category images in Medusa. You can expand on this by:

- Adding images to other models, such as collections.
- Adding support for reordering category images.
- Allowing setting multiple thumbnails for different use cases (for example, mobile vs. desktop).
- Adding alt text for category images for better accessibility and SEO.

### Learn More About Medusa

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md) for a more in-depth understanding of the concepts you've used in this guide and more.

To learn more about the commerce features Medusa provides, check out [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.


# Implement Customer Tiers in Medusa

In this tutorial, you'll learn how to implement a customer tiers system in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md), which are available out-of-the-box. These features include customer and promotion management capabilities.

A customer tiers system allows you to segment customers based on their purchase history and automatically apply promotions to their carts. Customers are assigned to tiers based on their total purchase value, and each tier can have an associated promotion that is automatically applied to their carts.

## Summary

By following this tutorial, you will learn how to:

- Install and set up Medusa.
- Create a Tier Module to manage customer tiers and tier rules.
- Customize the Medusa Admin to manage tiers.
- Automatically assign customers to tiers based on their purchase history.
- Automatically apply tier promotions to customer carts.
- Customize the Next.js Starter Storefront to display tier information to customers.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Diagram illustrating how the customer tiers system works, starting from the customer adding a product to the cart, Medusa applying the tier promotion automatically, customer placing the order, and Medusa updating the customer's tier.](https://res.cloudinary.com/dza7lstvk/image/upload/v1764079847/Medusa%20Resources/customer-tiers_hx504i.jpg)

- [Customer Tiers Repository](https://github.com/medusajs/examples/tree/main/customer-tiers): Find the full code for this guide in this repository.
- [OpenAPI Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1764080003/OpenApi/openapi_bzizgg.yaml): Import this OpenAPI Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.

Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Tier Module

In Medusa, you can build custom features in a [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without affecting your setup.

In the module, you define the data models necessary for a feature and the logic to manage these data models. Later, you can build commerce flows around your module.

In this step, you'll build a Tier Module that defines the necessary data models to store and manage customer tiers and tier rules.

Refer to the [Modules documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to learn more.

### Create Module Directory

Modules are created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/tier`.

### Create Data Models

A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML), which simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

Refer to the [Data Models documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md) to learn more.

For the Tier Module, you need to define two data models:

1. `Tier`: Represents a customer tier (for example, Bronze, Silver, Gold).
2. `TierRule`: Represents the rules that determine when a customer qualifies for a tier (for example, minimum purchase value in a specific currency).

So, create the file `src/modules/tier/models/tier.ts` with the following content:

```ts title="src/modules/tier/models/tier.ts"
import { model } from "@medusajs/framework/utils"
import { TierRule } from "./tier-rule"

export const Tier = model.define("tier", {
  id: model.id().primaryKey(),
  name: model.text(),
  promo_id: model.text().nullable(),
  tier_rules: model.hasMany(() => TierRule, {
    mappedBy: "tier",
  }),
})
```

You define the `Tier` data model using the `model.define` method of the DML. It accepts the data model's table name as a first parameter, and the model's schema object as a second parameter.

The `Tier` data model has the following properties:

- `id`: A unique ID for the tier.
- `name`: The name of the tier (for example, "Bronze", "Silver", "Gold").
- `promo_id`: The ID of the promotion associated with this tier.
- `tier_rules`: A one-to-many relationship with `TierRule` data model. Ignore the type error as you'll define the `TierRule` data model next.

Learn more about defining data model properties in the [Property Types documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md).

Next, create the file `src/modules/tier/models/tier-rule.ts` with the following content:

```ts title="src/modules/tier/models/tier-rule.ts"
import { model } from "@medusajs/framework/utils"
import { Tier } from "./tier"

export const TierRule = model.define("tier_rule", {
  id: model.id().primaryKey(),
  min_purchase_value: model.number(),
  currency_code: model.text(),
  tier: model.belongsTo(() => Tier, {
    mappedBy: "tier_rules",
  }),
})
.indexes([
  {
    on: ["tier_id", "currency_code"],
    unique: true,
  },
])
```

You define the `TierRule` data model with the following properties:

- `id`: A unique ID for the tier rule.
- `min_purchase_value`: The minimum purchase value required to qualify for the tier.
- `currency_code`: The currency code for which this rule applies (for example, `usd`, `eur`).
- `tier`: A many-to-one relationship with the `Tier` data model.

You also add a unique index on `tier_id` and `currency_code` to ensure that each tier has only one rule per currency.

Alternatively, you can store the minimum purchase value for a specific currency, then integrate with real-time exchange rate services to convert values between currencies. However, for simplicity, this tutorial uses fixed amounts for each currency.

### Create Module's Service

You now have the necessary data models in the Tier Module, but you'll need to manage their records. You do this by creating a service in the module.

A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database to manage your data models, or connect to a third-party service, which is useful when integrating with external systems.

Refer to the [Module Service documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md) to learn more.

To create the Tier Module's service, create the file `src/modules/tier/service.ts` with the following content:

```ts title="src/modules/tier/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import { Tier } from "./models/tier"
import { TierRule } from "./models/tier-rule"

class TierModuleService extends MedusaService({
  Tier,
  TierRule,
}) {
}

export default TierModuleService
```

The `TierModuleService` extends `MedusaService` from the Modules SDK, which generates a class with data-management methods for your module's data models. This saves you time implementing Create, Read, Update, and Delete (CRUD) methods.

So, the `TierModuleService` class now has methods like `createTiers`, `retrieveTier`, `listTierRules`, and more.

Find all methods generated by the `MedusaService` in [the Service Factory reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md).

### Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/tier/index.ts` with the following content:

```ts title="src/modules/tier/index.ts"
import TierModuleService from "./service"
import { Module } from "@medusajs/framework/utils"

export const TIER_MODULE = "tier"

export default Module(TIER_MODULE, {
  service: TierModuleService,
})
```

You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `tier`.
2. An object with a required property `service` indicating the module's service.

You also export the module's name as `TIER_MODULE` so you can reference it later.

### Add Module to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/tier",
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package's name.

### Generate Migrations

Since data models represent tables in the database, you define how to create them in the database using migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module.

Refer to the [Migrations documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md) to learn more.

Medusa's CLI tool can generate the migrations for you. To generate a migration for the Tier Module, run the following command in your Medusa application's directory:

```bash
npx medusa db:generate tier
```

The `db:generate` command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a `migrations` directory under `src/modules/tier` that holds the generated migration.

Then, to reflect these migrations on the database, run the following command:

```bash
npx medusa db:migrate
```

The tables for the `Tier` and `TierRule` data models are now created in the database.

***

## Step 3: Define Module Links

When you defined the `Tier` data model, you added properties that store IDs of records managed by other modules. For example, the `promo_id` property stores a promotion ID, but promotions are managed by the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md).

Medusa integrates modules into your application without side effects by isolating them from one another. This means you can't directly create relationships between data models in your module and data models in other modules.

Instead, Medusa provides a mechanism to define links between data models and to retrieve and manage linked records while maintaining module isolation. Links are useful for defining associations between data models in different modules or for extending a model in another module to associate custom properties with it.

Refer to the [Module Isolation documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md) to learn more.

In this step, you'll define:

1. A link between the Tier Module's `Tier` data model and the Customer Module's `Customer` data model.
2. A read-only link between the Tier Module's `Tier` data model and the Promotion Module's `Promotion` data model.

### Define Tier ↔ Customer Link

You can define links between data models in a TypeScript or JavaScript file under the `src/links` directory. So, create the file `src/links/tier-customer.ts` with the following content:

```ts title="src/links/tier-customer.ts"
import { defineLink } from "@medusajs/framework/utils"
import TierModule from "../modules/tier"
import CustomerModule from "@medusajs/medusa/customer"

export default defineLink(
  {
    linkable: TierModule.linkable.tier,
    filterable: ["id"],
  },
  {
    linkable: CustomerModule.linkable.customer,
    isList: true,
  }
)
```

You define a link using the `defineLink` function from the Modules SDK. It accepts two parameters:

1. An object indicating the first data model in the link. You pass the link configurations for the `Tier` data model from the Tier Module. You also specify the `id` property as filterable, allowing you to filter customers by their tier later using the [Index Module](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index-module/index.html.md).
2. An object indicating the second data model in the link. You pass the linkable configurations of the Customer Module's `Customer` data model. You set `isList` to `true` because a tier can have multiple customers.

This link allows you to retrieve and manage customers associated with a tier, and vice versa.

### Define Tier ↔ Promotion Link

Next, create the file `src/links/tier-promotion.ts` with the following content:

```ts title="src/links/tier-promotion.ts"
import { defineLink } from "@medusajs/framework/utils"
import TierModule from "../modules/tier"
import PromotionModule from "@medusajs/medusa/promotion"

export default defineLink(
  {
    linkable: TierModule.linkable.tier,
    field: "promo_id",
  },
  PromotionModule.linkable.promotion,
  {
    readOnly: true,
  }
)
```

You define a link between the `Tier` data model and the `Promotion` data model. You specify that the `promo_id` field in the `Tier` data model holds the ID of the linked promotion. You also set `readOnly` to `true` because you only want to retrieve the linked promotion without managing the link itself.

You can now retrieve the promotion associated with a tier, as you'll see in later steps.

***

## Step 4: Create Tier

Now that you have the Tier Module set up, you'll add the functionality to create tiers. This requires creating:

- A [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) with steps to create a tier.
- An [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) that exposes the workflow's functionality to client applications.

Later, you'll customize the Medusa Admin to allow creating tiers from the dashboard.

### a. Create Tier Workflow

To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task. You can track the workflow's execution progress, define rollback logic, and configure other advanced features.

Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

The workflow to create a tier has the following steps:

- [createTierStep](#createTierStep): Create the tier.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the created tier with its rules.

Medusa provides the last step out of the box. You'll create the other steps before creating the workflow.

#### Create Tier Step

First, you'll create a step that creates a tier. Create the file `src/workflows/steps/create-tier.ts` with the following content:

```ts title="src/workflows/steps/create-tier.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { TIER_MODULE } from "../../modules/tier"

export type CreateTierStepInput = {
  name: string
  promo_id: string | null
}

export const createTierStep = createStep(
  "create-tier",
  async (input: CreateTierStepInput, { container }) => {
    const tierModuleService = container.resolve(TIER_MODULE)

    const tier = await tierModuleService.createTiers({
      name: input.name,
      promo_id: input.promo_id || null,
    })

    return new StepResponse(tier, tier)
  },
  async (tier, { container }) => {
    if (!tier) {
      return
    }

    const tierModuleService = container.resolve(TIER_MODULE)
    await tierModuleService.deleteTiers(tier.id)
  }
)
```

You create a step with `createStep` from the Workflows SDK. It accepts three parameters:

1. The step's unique name, which is `create-tier`.
2. An async function that receives two parameters:
   - The step's input, which is in this case an object with the tier's properties.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.
3. An async compensation function that undoes the actions performed in the step if an error occurs during the workflow's execution.

In the step function, you resolve the Tier Module's service from the Medusa container and create the tier using the `createTiers` method.

A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters:

1. The step's output, which is the tier created.
2. Data to pass to the step's compensation function.

In the compensation function, you delete the tier if an error occurs during the workflow's execution.

#### Create Tier Rules Step

The `createTierRulesStep` creates tier rules for a tier.

Create the file `src/workflows/steps/create-tier-rules.ts` with the following content:

```ts title="src/workflows/steps/create-tier-rules.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { TIER_MODULE } from "../../modules/tier"

export type CreateTierRulesStepInput = {
  tier_id: string
  tier_rules: Array<{
    min_purchase_value: number
    currency_code: string
  }>
}

export const createTierRulesStep = createStep(
  "create-tier-rules",
  async (input: CreateTierRulesStepInput, { container }) => {
    const tierModuleService = container.resolve(TIER_MODULE)

    const createdRules = await tierModuleService.createTierRules(
      input.tier_rules.map((rule) => ({
        tier_id: input.tier_id,
        min_purchase_value: rule.min_purchase_value,
        currency_code: rule.currency_code,
      }))
    )

    return new StepResponse(createdRules, createdRules)
  },
  async (createdRules, { container }) => {
    if (!createdRules?.length) {
      return
    }

    const tierModuleService = container.resolve(TIER_MODULE)
    await tierModuleService.deleteTierRules(createdRules.map((rule) => rule.id))
  }
)
```

This step receives the rules to create with the ID of the tier they belong to.

In the step function, you create the tier rules. In the compensation function, you delete them if an error occurs during the workflow's execution.

#### Create Tier Workflow

You can now create the workflow that creates a tier.

Create the file `src/workflows/create-tier.ts` with the following content:

```ts title="src/workflows/create-tier.ts" collapsibleLines="1-10" expandButtonLabel="Show Imports"
import {
  createWorkflow,
  WorkflowResponse,
  transform,
  when,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { createTierStep } from "./steps/create-tier"
import { createTierRulesStep } from "./steps/create-tier-rules"

export type CreateTierWorkflowInput = {
  name: string
  promo_id?: string | null
  tier_rules?: Array<{
    min_purchase_value: number
    currency_code: string
  }>
}

export const createTierWorkflow = createWorkflow(
  "create-tier",
  (input: CreateTierWorkflowInput) => {
    // Validate promotion if provided
    when({ input }, (data) => !!data.input.promo_id)
      .then(() => {
        useQueryGraphStep({
          entity: "promotion",
          fields: ["id"],
          filters: {
            id: input.promo_id!,
          },
          options: {
            throwIfKeyNotFound: true,
          },
        })
      })
    // Create the tier
    const tier = createTierStep({
      name: input.name,
      promo_id: input.promo_id || null,
    })

    // Create tier rules if provided
    when({ input }, (data) => {
      return !!data.input.tier_rules?.length
    }).then(() => {
      return createTierRulesStep({
        tier_id: tier.id,
        tier_rules: input.tier_rules!,
      })
    })

    // Retrieve the created tier with rules
    const { data: tiers } = useQueryGraphStep({
      entity: "tier",
      fields: ["*", "tier_rules.*"],
      filters: {
        id: tier.id,
      },
    }).config({ name: "retrieve-tier" })

    return new WorkflowResponse({
      tier: tiers[0],
    })
  }
)
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is an object with the tier's details.

In the workflow's constructor function, you:

- Use [when-then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) to check whether the promotion ID is provided and retrieve the promotion to validate that it exists.
  - By specifying the `throwIfKeyNotFound` option, the `useQueryGraphStep` throws an error if the promotion isn't found, which stops the workflow's execution.
  - This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) under the hood to retrieve data across modules.
- Create the tier using the `createTierStep`.
- Use [when-then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) to conditionally create tier rules if they're provided using the `createTierRulesStep`.
- Retrieve the created tier with its rules using `useQueryGraphStep`.

Finally, you return a `WorkflowResponse` with the created tier.

In workflows, you need `when-then` to check conditions based on execution values. Learn more in the [Conditions](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) workflow documentation.

### b. Create Tier API Route

Now that you have the workflow to create tiers, you'll create an API route that exposes this functionality to client applications.

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

Refer to the [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) documentation to learn more about them.

Create the file `src/api/admin/tiers/route.ts` with the following content:

```ts title="src/api/admin/tiers/route.ts"
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { z } from "zod"
import { createTierWorkflow } from "../../../workflows/create-tier"

export const CreateTierSchema = z.object({
  name: z.string(),
  promo_id: z.string().nullable(),
  tier_rules: z.array(z.object({
    min_purchase_value: z.number(),
    currency_code: z.string(),
  })),
})

type CreateTierInput = z.infer<typeof CreateTierSchema>

export async function POST(
  req: MedusaRequest<CreateTierInput>,
  res: MedusaResponse
): Promise<void> {
  const { name, promo_id, tier_rules } = req.validatedBody

  const { result } = await createTierWorkflow(req.scope).run({
    input: {
      name,
      promo_id: promo_id || null,
      tier_rules: tier_rules || [],
    },
  })

  res.json({ tier: result.tier })
}
```

First, you define a Zod schema that validates the request body.

Then, you export a `POST` function, which exposes a `POST` API route at `/admin/tiers`.

In the route handler function, you execute the `createTierWorkflow` by invoking it, passing it the Medusa container, then executing its `run` method.

You return the created tier in the response.

You'll test out this API route later when you customize the Medusa Admin dashboard.

### c. Apply Validation Middleware

To ensure incoming request bodies are validated, you need to apply a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

To apply a middleware to the API route, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import {
  defineMiddlewares,
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { CreateTierSchema } from "./admin/tiers/route"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/admin/tiers",
      methods: ["POST"],
      middlewares: [validateAndTransformBody(CreateTierSchema)],
    },
  ],
})
```

You apply the `validateAndTransformBody` middleware to the `POST` route of the `/admin/tiers` path, passing it the Zod schema you created in the route file.

Any request that doesn't conform to the schema will receive a `400` Bad Request response.

Refer to the [Middlewares](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) documentation to learn more.

***

## Step 5: Retrieve Tiers API Route

In this step, you'll add an API route that retrieves tiers. You'll use this API route later when you customize the Medusa Admin to display tiers in the Medusa Admin.

To create the API route, add the following function to the `src/api/admin/tiers/route.ts` file:

```ts title="src/api/admin/tiers/route.ts"
export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
): Promise<void> {
  const query = req.scope.resolve("query")

  const { data: tiers, metadata } = await query.graph({
    entity: "tier",
    ...req.queryConfig,
  })

  res.json({
    tiers,
    count: metadata?.count || 0,
    offset: metadata?.skip || 0,
    limit: metadata?.take || 15,
  })
}
```

You export a `GET` route handler function, which will expose a `GET` API route at `/admin/tiers`.

In the route handler, you resolve Query from the Medusa container and use it to retrieve a list of tiers.

Notice that you spread the `req.queryConfig` object into the `query.graph` method. This allows clients to pass query parameters for pagination and configure returned fields. You'll learn how to set these configurations in a bit.

You return the list of tiers in the response.

You'll test out this API route later when you customize the Medusa Admin dashboard.

### Apply Query Configurations Middleware

Next, you need to apply a middleware that validates the query parameters passed to the request, and sets the default Query configurations.

In `src/api/middlewares.ts`, add the following imports at the top of the file:

```ts title="src/api/middlewares.ts"
import {
  validateAndTransformQuery,
} from "@medusajs/framework/http"
import { createFindParams } from "@medusajs/medusa/api/utils/validators"
```

Then, add the following object to the `routes` array passed to `defineMiddlewares`:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/tiers",
      methods: ["GET"],
      middlewares: [validateAndTransformQuery(createFindParams(), {
        isList: true,
        defaults: ["id", "name", "promotion.id", "promotion.code"],
      })],
    },
  ],
})
```

You apply the `validateAndTransformQuery` middleware to `GET` requests sent to the `/admin/tiers` route, passing it the `createFindParams` utility function to create a schema that validates common query parameters like `limit`, `offset`, `fields`, and `order`.

You set the following configurations:

- `isList`: Set to `true` to indicate that the API route returns a list of records.
- `defaults`: An array of fields to return by default if the client doesn't specify any fields in the request.

Refer to the [Request Query Configuration](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#request-query-configurations/index.html.md) documentation to learn more about this middleware and the query configurations.

***

## Step 6: Manage Customer Tiers in Medusa Admin

In this step, you'll customize the Medusa Admin to display and create tiers.

The Medusa Admin dashboard is customizable, allowing you to insert widgets into existing pages, or create new pages.

Refer to the [Admin Development](https://docs.medusajs.com/docs/learn/fundamentals/admin/index.html.md) documentation to learn more.

### a. Initialize JS SDK

To send requests to the Medusa server, you'll use the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md). It's already installed in your Medusa project, but you need to initialize it before using it in your customizations.

Create the file `src/admin/lib/sdk.ts` with the following content:

```ts title="src/admin/lib/sdk.ts" highlights={sdkHighlights}
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: process.env.MEDUSA_BACKEND_URL || "http://localhost:9000",
  debug: process.env.NODE_ENV === "development",
  auth: {
    type: "session",
  },
})
```

Learn more about the initialization options in the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) reference.

### b. Tiers UI Route

Next, you'll create a UI route that displays the list of tiers in the Medusa Admin.

A UI route is a React component that specifies the content to be shown in a new page in the Medusa Admin dashboard.

Learn more about UI routes in the [UI Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md).

To create the UI route, create the file `src/admin/routes/tiers/page.tsx` with the following content:

```tsx title="src/admin/routes/tiers/page.tsx" collapsibleLines="1-16" expandButtonLabel="Show Imports"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import {
  Container,
  Heading,
  Button,
  DataTable,
  createDataTableColumnHelper,
  useDataTable,
  DataTablePaginationState,
} from "@medusajs/ui"
import { useNavigate, Link } from "react-router-dom"
import { UserGroup } from "@medusajs/icons"
import { useQuery } from "@tanstack/react-query"
import { useState, useMemo } from "react"
import { sdk } from "../../lib/sdk"

export type Tier = {
  id: string
  name: string
  promotion: {
    id: string
    code: string
  } | null
  tier_rules: Array<{
    id: string
    min_purchase_value: number
    currency_code: string
  }>
}

type TiersResponse = {
  tiers: Tier[]
  count: number
  offset: number
  limit: number
}

const columnHelper = createDataTableColumnHelper<Tier>()

const columns = [
  columnHelper.accessor("name", {
    header: "Name",
    enableSorting: true,
  }),
  columnHelper.accessor("promotion", {
    header: "Promotion",
    cell: ({ getValue }) => {
      const promotion = getValue()
      return promotion ? <Link to={`/promotions/${promotion.id}`}>{promotion.code}</Link> : "-"
    },
  }),
]

const TiersPage = () => {
  const navigate = useNavigate()
  const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)
  const limit = 15
  const [pagination, setPagination] = useState<DataTablePaginationState>({
    pageSize: limit,
    pageIndex: 0,
  })

  const offset = useMemo(() => {
    return pagination.pageIndex * limit
  }, [pagination])

  const { data, isLoading } = useQuery({
    queryFn: () =>
      sdk.client.fetch<TiersResponse>("/admin/tiers", {
        method: "GET",
        query: {
          limit,
          offset,
        },
      }),
    queryKey: ["tiers", "list", limit, offset],
  })

  const tiers = data?.tiers || []

  const table = useDataTable({
    columns,
    data: tiers,
    getRowId: (tier) => tier.id,
    rowCount: data?.count || 0,
    isLoading,
    pagination: {
      state: pagination,
      onPaginationChange: setPagination,
    },
    onRowClick: (_event, row) => {
      // TODO navigate to the tier details page
    },
  })

  return (
    <Container className="divide-y p-0">
      <DataTable instance={table}>
        <DataTable.Toolbar className="flex items-center justify-between px-6 py-4">
          <Heading level="h1">Customer Tiers</Heading>
          <Button onClick={() => setIsCreateModalOpen(true)}>
            Create Tier
          </Button>
        </DataTable.Toolbar>
        <DataTable.Table />
        <DataTable.Pagination />
      </DataTable>
      {/* TODO show create tier modal */}
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Customer Tiers",
  icon: UserGroup,
})

export default TiersPage
```

A UI route file must export a React component as the default export. This component is rendered when the user navigates to the UI route. It can also export a route configuration object that defines the UI route's label and icon in the sidebar.

In the component, you:

- Define state variables to configure pagination.
- Fetch the tiers using the JS SDK and Tanstack Query. By using Tanstack Query, you can easily manage the data fetching state, handle pagination, and cache the data.
- Create a DataTable instance from [Medusa UI](https://docs.medusajs.com/ui/index.html.md). You pass the columns, data, and pagination configurations to the hook.
- Render the DataTable component with a toolbar and pagination controls.

### c. Create Tier Modal Component

Next, you'll create a component that shows a form to create a tier in a modal.

Create the file `src/admin/components/create-tier-modal.tsx` with the following content:

```tsx title="src/admin/components/create-tier-modal.tsx" collapsibleLines="1-9" expandButtonLabel="Show Imports"
import { FocusModal, Heading, Label, Input, Button, Select, IconButton, toast } from "@medusajs/ui"
import { Trash } from "@medusajs/icons"
import { useForm, Controller, FormProvider } from "react-hook-form"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"
import { useState } from "react"
import { useNavigate } from "react-router-dom"
import { Tier } from "../routes/tiers/page"

type CreateTierFormData = {
  name: string
  promo_id: string | null
  tier_rules: Array<{
    min_purchase_value: number
    currency_code: string
  }>
}

type CreateTierModalProps = {
  open: boolean
  onOpenChange: (open: boolean) => void
}

export const CreateTierModal = ({ open, onOpenChange }: CreateTierModalProps) => {
  const navigate = useNavigate()
  const queryClient = useQueryClient()
  const [tierRules, setTierRules] = useState<{
    currency_code: string
    min_purchase_value: number
  }[]>([])

  const form = useForm<CreateTierFormData>({
    defaultValues: {
      name: "",
      promo_id: null,
      tier_rules: [],
    },
  })

  // TODO add queries and mutations
}
```

You define a component that receives the modal's open state and a function to close it.

In the component, so far you define necessary variables and initialize the form.

Next, you'll define Tanstack queries and mutations to retrieve form data and create the tier. Replace the `TODO` with the following:

```tsx title="src/admin/components/create-tier-modal.tsx"
const { data: promotionsData } = useQuery({
  queryFn: () => sdk.admin.promotion.list(),
  queryKey: ["promotions", "list"],
})

const { data: storeData } = useQuery({
  queryFn: () =>
    sdk.admin.store.list({
      fields: "id,supported_currencies.*,supported_currencies.currency.*",
    }),
  queryKey: ["store"],
})

const createTierMutation = useMutation({
  mutationFn: async (data: CreateTierFormData) => {
    return await sdk.client.fetch<{ tier: Tier }>("/admin/tiers", {
      method: "POST",
      body: data,
    })
  },
  onSuccess: (data: { tier: Tier }) => {
    queryClient.invalidateQueries({ queryKey: ["tiers"] })
    form.reset()
    setTierRules([])
    onOpenChange(false)
    // TODO navigate to the new tier page
    toast.success("Success", {
      description: "Tier created successfully",
      position: "top-right",
    })
  },
  onError: (error) => {
    toast.error("Error", {
      description: error.message,
      position: "top-right",
    })
  },
})

// TODO and function handlers
```

You retrieve the promotions and store data to populate the form with promotions and supported currencies for rules.

You also define a mutation to create a tier using the `useMutation` hook from Tanstack Query.

Next, you'll add functions to handle form submissions and other actions. Replace the `TODO` with the following:

```tsx title="src/admin/components/create-tier-modal.tsx"
const handleSubmit = form.handleSubmit((data) => {
  createTierMutation.mutate({
    ...data,
    tier_rules: tierRules,
  })
})

const promotions = promotionsData?.promotions || []
const store = storeData?.stores?.[0]
const supportedCurrencies = store?.supported_currencies || []

const getAvailableCurrencies = () => {
  const usedCurrencies = new Set(tierRules.map((rule) => rule.currency_code))
  return supportedCurrencies.filter((sc) => !usedCurrencies.has(sc.currency_code))
}

const addTierRule = () => {
  const availableCurrencies = getAvailableCurrencies()
  if (availableCurrencies.length > 0) {
    const firstCurrency = availableCurrencies[0].currency_code
    setTierRules([
      ...tierRules,
      {
        currency_code: firstCurrency,
        min_purchase_value: 0,
      },
    ])
  }
}

const removeTierRule = (index: number) => {
  setTierRules(tierRules.filter((_, i) => i !== index))
}

const updateTierRule = (index: number, field: "currency_code" | "min_purchase_value", value: string | number) => {
  const updated = [...tierRules]
  updated[index] = {
    ...updated[index],
    [field]: value,
  }
  setTierRules(updated)
}

// TODO add return statement
```

You define the following functions:

- `handleSubmit`: Handles form submissions by calling the `createTierMutation`, passing it the form data and tier rules.
- `getAvailableCurrencies`: Returns the available currencies that haven't been used yet in the tier rules.
- `addTierRule`: Adds a new tier rule to the form.
- `removeTierRule`: Removes a tier rule from the form.
- `updateTierRule`: Updates a tier rule in the form.

Finally, replace the `TODO` with the following return statement to render the modal:

```tsx title="src/admin/components/create-tier-modal.tsx"
return (
  <FocusModal open={open} onOpenChange={onOpenChange}>
    <FocusModal.Content>
      <FormProvider {...form}>
        <form onSubmit={handleSubmit} className="flex h-full flex-col overflow-hidden">
          <FocusModal.Header>
          <div className="flex items-center justify-between">
            <Heading level="h1">Create Tier</Heading>
          </div>
        </FocusModal.Header>
        <FocusModal.Body className="flex flex-1 flex-col overflow-y-auto">
          <div className="mx-auto flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
            <div className="flex flex-col gap-y-4">
              <Controller
                control={form.control}
                name="name"
                rules={{ required: "Name is required" }}
                render={({ field }) => (
                  <div className="flex flex-col gap-y-2">
                    <Label size="small" weight="plus">
                      Name
                    </Label>
                    <Input {...field} placeholder="e.g., Bronze, Silver, Gold" />
                  </div>
                )}
              />

              <Controller
                control={form.control}
                name="promo_id"
                render={({ field }) => (
                  <div className="flex flex-col gap-y-2">
                    <Label size="small" weight="plus">
                      Promotion (Optional)
                    </Label>
                    <Select
                      value={field.value || ""}
                      onValueChange={(value) => field.onChange(value || null)}
                    >
                      <Select.Trigger>
                        <Select.Value placeholder="Select a promotion" />
                      </Select.Trigger>
                      <Select.Content>
                        {promotions.map((promo) => (
                          <Select.Item key={promo.id} value={promo.id}>
                            {promo.code}
                          </Select.Item>
                        ))}
                      </Select.Content>
                    </Select>
                  </div>
                )}
              />

              <div className="flex flex-col gap-y-4">
                <div className="flex items-center justify-between">
                  <Label size="small" weight="plus">
                    Tier Rules
                  </Label>
                  <Button
                    type="button"
                    variant="secondary"
                    size="small"
                    onClick={addTierRule}
                    disabled={getAvailableCurrencies().length === 0}
                  >
                    Add Rule
                  </Button>
                </div>

                {tierRules.length === 0 && (
                  <div className="text-sm text-gray-500">
                    No tier rules added. Click "Add Rule" to add a rule for a currency.
                  </div>
                )}

                {tierRules.map((rule, index) => (
                  <div key={index} className="flex items-end gap-x-2 rounded-lg border p-4">
                    <div className="flex flex-1 flex-col gap-y-2">
                      <Label size="small">Currency</Label>
                      <Select
                        value={rule.currency_code}
                        onValueChange={(value) => updateTierRule(index, "currency_code", value)}
                      >
                        <Select.Trigger>
                          <Select.Value placeholder="Select currency" />
                        </Select.Trigger>
                        <Select.Content>
                          {supportedCurrencies
                            .filter((sc) => {
                              // Allow current selection or currencies not used in other rules
                              return (
                                sc.currency_code === rule.currency_code ||
                                !tierRules.some(
                                  (r, i) => i !== index && r.currency_code === sc.currency_code
                                )
                              )
                            })
                            .map((sc) => (
                              <Select.Item key={sc.currency_code} value={sc.currency_code}>
                                {sc.currency.code.toUpperCase()} - {sc.currency.name}
                              </Select.Item>
                            ))}
                        </Select.Content>
                      </Select>
                    </div>
                    <div className="flex flex-1 flex-col gap-y-2">
                      <Label size="small">Minimum Purchase Value</Label>
                      <Input
                        type="number"
                        min="0"
                        step="0.01"
                        value={rule.min_purchase_value}
                        onChange={(e) =>
                          updateTierRule(index, "min_purchase_value", parseFloat(e.target.value) || 0)
                        }
                      />
                    </div>
                    <IconButton
                      type="button"
                      variant="transparent"
                      size="small"
                      onClick={() => removeTierRule(index)}
                    >
                      <Trash />
                    </IconButton>
                  </div>
                ))}
              </div>
            </div>
          </div>
        </FocusModal.Body>
        <FocusModal.Footer>
          <div className="flex items-center gap-x-2">
            <FocusModal.Close asChild>
              <Button variant="secondary" size="small">
                Cancel
              </Button>
            </FocusModal.Close>
            <Button type="submit" size="small" isLoading={createTierMutation.isPending}>
              Create
            </Button>
          </div>
        </FocusModal.Footer>
        </form>
      </FormProvider>
    </FocusModal.Content>
  </FocusModal>
)
```

You display a `FocusModal` from Medusa UI. In the modal, you render a form with the following fields:

1. **Name**: The name of the tier.
2. **Promotion**: A select input to choose a promotion that's associated with the tier.
3. **Tier Rules**: A list of inputs to specify the minimum purchase value required in a specific currency to qualify for the tier.

### d. Show Create Tier Modal

Next, you'll show the create tier modal when the user clicks the "Create Tier" button in the tiers page.

First, add the following import at the top of `src/admin/routes/tiers/page.tsx`:

```tsx title="src/admin/routes/tiers/page.tsx"
import { CreateTierModal } from "../../components/create-tier-modal"
```

Next, replace the `TODO` in the `TiersPage` component's `return` statement with the following:

```tsx title="src/admin/routes/tiers/page.tsx"
<CreateTierModal open={isCreateModalOpen} onOpenChange={setIsCreateModalOpen} />
```

You display the create tier modal when the user clicks the "Create Tier" button in the tiers page.

### Test Customer Tiers in Medusa Admin

To test out the customer tiers in the Medusa Admin, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and log in using the credentials you set up earlier.

You'll find a new sidebar item labeled "Customer Tiers." Click on it to view the list of tiers.

![Customer Tiers page showing list of tiers](https://res.cloudinary.com/dza7lstvk/image/upload/v1764065196/Medusa%20Resources/CleanShot_2025-11-25_at_12.05.51_2x_u2khaj.png)

Before creating a tier, you should [create a promotion](https://docs.medusajs.com/user-guide/promotions/create/index.html.md).

Then, on the Customer Tiers page, click the "Create Tier" button to open the create tier modal.

In the modal, enter the tier's name, select the promotion you created, and add tier rules for the currencies in your store. Once you're done, click the "Create" button to create the tier.

![Create tier modal showing form to create a tier](https://res.cloudinary.com/dza7lstvk/image/upload/v1764065361/Medusa%20Resources/CleanShot_2025-11-25_at_12.08.18_2x_nid87p.png)

You'll see the new tier in the list of tiers. Later, you'll add a page to view and edit a single tier's details.

***

## Step 7: Retrieve Tier API Route

In this step, you'll add an API route that retrieves a tier.

To create the API route, create the file `src/api/admin/tiers/[id]/route.ts` with the following content:

```ts title="src/api/admin/tiers/[id]/route.ts"
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
): Promise<void> {
  const query = req.scope.resolve("query")
  const { id } = req.params

  const { data: tiers } = await query.graph({
    entity: "tier",
    filters: {
      id,
    },
    ...req.queryConfig,
  }, {
    throwIfKeyNotFound: true,
  })

  res.json({ tier: tiers[0] })
}
```

You export a `GET` route handler function, which will expose a `GET` API route at `/admin/tiers/:id`.

In the route handler, you resolve Query from the Medusa container and use it to retrieve the tier with the given ID.

You return the tier in the response.

You'll test out this API route later when you customize the Medusa Admin dashboard.

### Apply Query Configurations Middleware

Next, you need to apply a middleware that validates the query parameters passed to the request, and sets the default Query configurations.

In `src/api/middlewares.ts`, add the following object to the `routes` array passed to `defineMiddlewares`:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/tiers/:id",
      methods: ["GET"],
      middlewares: [
        validateAndTransformQuery(createFindParams(), {
          isList: false,
          defaults: ["id", "name", "promotion.id", "promotion.code", "tier_rules.*"],
        }),
      ],
    },
  ],
})
```

Similar to before, you define the query configurations for the `GET` request to the `/admin/tiers/:id` route.

***

## Step 8: Update Tier

In this step, you'll add the functionality to update tiers. This includes creating a workflow to update a tier and an API route that executes it.

### a. Update Tier Workflow

The workflow to update a tier has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the tier to update.
- [updateTierStep](#updateTierStep): Update the tier.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the updated tier with rules.

Medusa provides the `useQueryGraphStep` out-of-the-box, and you've implemented the `createTierRulesStep`. You'll create the other steps before creating the workflow.

#### Update Tier Step

The `updateTierStep` updates a tier.

To create the step, create the file `src/workflows/steps/update-tier.ts` with the following content:

```ts title="src/workflows/steps/update-tier.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { TIER_MODULE } from "../../modules/tier"
import TierModuleService from "../../modules/tier/service"

export type UpdateTierStepInput = {
  id: string
  name: string
  promo_id: string | null
}

export const updateTierStep = createStep(
  "update-tier",
  async (input: UpdateTierStepInput, { container }) => {
    const tierModuleService: TierModuleService = container.resolve(TIER_MODULE)

    const originalTier = await tierModuleService.retrieveTier(input.id)

    const tier = await tierModuleService.updateTiers(input)

    return new StepResponse(tier, originalTier)
  },
  async (originalInput, { container }) => {
    if (!originalInput) {
      return
    }

    const tierModuleService = container.resolve(TIER_MODULE)
    
    await tierModuleService.updateTiers({
      id: originalInput.id,
      name: originalInput.name,
      promo_id: originalInput.promo_id,
    })
  }
)
```

The step receives the tier's ID and the details to update.

In the step function, you retrieve the original tier, then you update it. You pass the original tier details to the compensation function.

In the compensation function, you restore the original tier details if an error occurs during the workflow's execution.

#### Delete Tier Rules Step

The `deleteTierRulesStep` deletes tier rules.

To create the step, create the file `src/workflows/steps/delete-tier-rules.ts` with the following content:

```ts title="src/workflows/steps/delete-tier-rules.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { TIER_MODULE } from "../../modules/tier"
import TierModuleService from "../../modules/tier/service"

export type DeleteTierRulesStepInput = {
  ids: string[]
}

export const deleteTierRulesStep = createStep(
  "delete-tier-rules",
  async (input: DeleteTierRulesStepInput, { container }) => {
    const tierModuleService: TierModuleService = container.resolve(TIER_MODULE)

    // Get existing rules
    const existingRules = await tierModuleService.listTierRules({
      id: input.ids,
    })

    // Delete all rules
    await tierModuleService.deleteTierRules(input.ids)

    return new StepResponse(void 0, existingRules)
  },
  async (compensationData, { container }) => {
    if (!compensationData?.length) {
      return
    }

    const tierModuleService: TierModuleService = container.resolve(TIER_MODULE)
    // Restore deleted rules
    await tierModuleService.createTierRules(
      compensationData.map((rule) => ({
        tier_id: rule.tier_id,
        min_purchase_value: rule.min_purchase_value,
        currency_code: rule.currency_code,
      }))
    )
  }
)
```

The step receives the IDs of the tier rules to delete.

In the step function, you retrieve the existing rules, then you delete them. You pass the existing rules to the compensation function.

In the compensation function, you restore the existing rules if an error occurs during the workflow's execution.

#### Update Tier Workflow

You can now create the workflow that updates a tier.

Create the file `src/workflows/update-tier.ts` with the following content:

```ts title="src/workflows/update-tier.ts" collapsibleLines="1-11" expandButtonLabel="Show Imports"
import {
  createWorkflow,
  WorkflowResponse,
  transform,
  when,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { updateTierStep } from "./steps/update-tier"
import { deleteTierRulesStep } from "./steps/delete-tier-rules"
import { createTierRulesStep } from "./steps/create-tier-rules"

export type UpdateTierWorkflowInput = {
  id: string
  name: string
  promo_id?: string | null
  tier_rules?: Array<{
    min_purchase_value: number
    currency_code: string
  }>
}

export const updateTierWorkflow = createWorkflow(
  "update-tier",
  (input: UpdateTierWorkflowInput) => {
    const { data: tiers } = useQueryGraphStep({
      entity: "tier",
      fields: ["tier_rules.*"],
      filters: {
        id: input.id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })
    // Validate promotion if provided
    when({ input }, (data) => !!data.input.promo_id)
      .then(() => {
        useQueryGraphStep({
          entity: "promotion",
          fields: ["id"],
          filters: {
            id: input.promo_id!,
          },
          options: {
            throwIfKeyNotFound: true,
          },
        }).config({ name: "retrieve-promotion" })
      })
    // Update the tier
    updateTierStep({
      id: input.id,
      name: input.name,
      promo_id: input.promo_id || null,
    })

    when({ input }, (data) => {
      return !!data.input.tier_rules?.length
    }).then(() => {
      const ids = transform({
        tiers,
      }, (data) => {
        return (data.tiers[0].tier_rules?.map((rule) => rule?.id) || []) as string[]
      })
      deleteTierRulesStep({
        ids,
      })
      return createTierRulesStep({
        tier_id: input.id,
        tier_rules: input.tier_rules!,
      })
    })

    // Retrieve the updated tier with rules
    const { data: updatedTiers } = useQueryGraphStep({
      entity: "tier",
      fields: ["*", "tier_rules.*"],
      filters: {
        id: input.id,
      },
    }).config({ name: "updated-tier" })

    return new WorkflowResponse({
      tier: updatedTiers[0],
    })
  }
)
```

The workflow receives the details to update the tier.

In the workflow, you:

1. Retrieve the tier to update using the `useQueryGraphStep`.
2. Use `when-then` to check if the promotion ID is provided. If so, use `useQueryGraphStep` to validate that it exists.
3. Update the tier using the `updateTierStep`.
4. If new tier rules are provided, you:
   - delete the existing tier rules using the `deleteTierRulesStep`.
   - Create the new tier rules using the `createTierRulesStep`.
5. Retrieve the updated tier with rules using the `useQueryGraphStep`.

Finally, you return a `WorkflowResponse` with the updated tier.

In workflows, you need `transform` to prepare data based on execution values. Learn more in the [Data Manipulation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) workflow documentation.

### b. Update Tier API Route

Next, you'll create the API route that exposes the workflow's functionality to client applications.

In `src/api/admin/tiers/[id]/route.ts`, add the following imports at the top of the file:

```ts title="src/api/admin/tiers/[id]/route.ts"
import { z } from "zod"
import { updateTierWorkflow } from "../../../../workflows/update-tier"
```

Then, add the following at the end of the file:

```ts title="src/api/admin/tiers/[id]/route.ts"
export const UpdateTierSchema = z.object({
  name: z.string(),
  promo_id: z.string().nullable(),
  tier_rules: z.array(z.object({
    min_purchase_value: z.number(),
    currency_code: z.string(),
  })),
})

type UpdateTierInput = z.infer<typeof UpdateTierSchema>

export async function POST(
  req: MedusaRequest<UpdateTierInput>,
  res: MedusaResponse
): Promise<void> {
  const { id } = req.params
  const { name, promo_id, tier_rules } = req.validatedBody

  const { result } = await updateTierWorkflow(req.scope).run({
    input: {
      id,
      name,
      promo_id: promo_id !== undefined ? promo_id : null,
      tier_rules: tier_rules || [],
    },
  })

  res.json({ tier: result.tier })
}
```

You define a Zod schema that validates the request body.

Then, you export a `POST` function, which exposes a `POST` API route at `/admin/tiers/:id`.

In the route handler, you execute the `updateTierWorkflow` and return the updated tier in the response.

You'll test out the API route later when you customize the Medusa Admin dashboard.

#### c. Apply Validation Middleware

Next, you'll apply a validation middleware to the API route.

In `src/api/middlewares.ts`, add the following import at the top of the file:

```ts title="src/api/middlewares.ts"
import { UpdateTierSchema } from "./admin/tiers/[id]/route"
```

Then, add a new route object passed to the array in `defineMiddlewares`:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/tiers/:id",
      methods: ["POST"],
      middlewares: [validateAndTransformBody(UpdateTierSchema)],
    },
  ],
})
```

You apply the `validateAndTransformBody` middleware to the `POST` route of the `/admin/tiers/:id` path, passing it the Zod schema you created in the route file.

Any request that doesn't conform to the schema will receive a `400` Bad Request response.

***

## Step 9: Retrieve Customers in Tier API Route

In this step, you'll add an API route that retrieves customers in a tier. This will be useful to show the customers in the tier's page on the Medusa Admin.

### a. Retrieve Customers in Tier API Route

To create the API route, create the file `src/api/admin/tiers/[id]/customers/route.ts` with the following content:

```ts title="src/api/admin/tiers/[id]/customers/route.ts"
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
): Promise<void> {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
  const { id } = req.params

  // Query customers linked to this tier
  const { data: customers, metadata } = await query.index({
    entity: "customer",
    filters: {
      tier: {
        id,
      },
    },
    ...req.queryConfig,
  })

  res.json({
    customers,
    count: metadata?.estimate_count || 0,
    offset: metadata?.skip || 0,
    limit: metadata?.take || 15,
  })
}
```

You export a `GET` function, which exposes a `GET` API route at `/admin/tiers/:id/customers`.

In the route handler, you resolve Query from the Medusa container and use it to retrieve customers linked to the tier with the given ID.

Notice that you use the `query.index` method. This method is similar to `query.graph` but allows you to filter by linked records using the [Index Module](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index-module/index.html.md).

You return the customers in the response.

You'll test out this API route later when you customize the Medusa Admin dashboard.

### b. Apply Query Configurations Middleware

Next, you need to apply a middleware that validates the query parameters passed to the request, and sets the default Query configurations.

In `src/api/middlewares.ts`, add the following object to the `routes` array passed to `defineMiddlewares`:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/tiers/:id/customers",
      methods: ["GET"],
      middlewares: [
        validateAndTransformQuery(createFindParams(), {
          isList: true,
          defaults: ["id", "email", "first_name", "last_name"],
        }),
      ],
    },
  ],
})
```

You apply the `validateAndTransformQuery` middleware to the `GET` route of the `/admin/tiers/:id/customers` path, passing it the `createFindParams` utility function to create a schema that validates common query parameters like `limit`, `offset`, `fields`, and `order`.

You set the following configurations:

- `isList`: Set to `true` to indicate that the API route returns a list of records.
- `defaults`: An array of fields to return by default if the client doesn't specify any fields in the request.

### c. Install Index Module

The [Index Module](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index-module/index.html.md) is a tool for performing high-performance queries across modules, such as filtering linked modules.

The Index Module is currently experimental, so you need to install and configure it manually.

To install the Index Module, run the following command in your Medusa application's directory:

```bash npm2yarn
npm install @medusajs/index
```

Then, add the following to your Medusa application's configuration file:

```ts title="medusa-config.ts"
export default config({
  modules: [
    // ...
    {
      resolve: "@medusajs/index",
    },
  ],
})
```

Next, run the migrations to create the necessary tables for the Index Module in your database:

```bash npm2yarn
npx medusa db:migrate
```

Lastly, start the Medusa application to ingest the data into the Index Module:

```bash npm2yarn
npm run dev
```

You can now use the Index Module to filter customers by their tier. You'll test out the API route when you customize the Medusa Admin dashboard in the next step.

Refer to the [Index Module](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index-module/index.html.md) documentation to learn more.

***

## Step 10: Tier Details UI Route

In this step, you'll create a UI route that displays the details of a tier.

The UI route is composed of three sections:

- Tier Details Section: This also includes a form to edit the tier's details.
- Tier Rules Table
- Tier Customers Table

You'll create the components for each section first, then you'll create the UI route.

### a. Edit Tier Drawer Component

You'll first create a drawer component that displays a form to edit the tier's details. You'll then display the component in the Tier Details Section.

To create the drawer component, create the file `src/admin/components/edit-tier-drawer.tsx` with the following content:

```tsx title="src/admin/components/edit-tier-drawer.tsx"
import { Drawer, Heading, Label, Input, Button, Select, IconButton, toast } from "@medusajs/ui"
import { useForm, Controller, FormProvider } from "react-hook-form"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"
import { useState, useEffect } from "react"
import { Tier } from "../routes/tiers/page"
import { Trash } from "@medusajs/icons"

type EditTierFormData = {
  name: string
  promo_id: string | null
  tier_rules: Array<{
    min_purchase_value: number
    currency_code: string
  }>
}

type EditTierDrawerProps = {
  tier: Tier | undefined
}

export const EditTierDrawer = ({ tier }: EditTierDrawerProps) => {
  const queryClient = useQueryClient()
  const [open, setOpen] = useState(false)
  const [tierRules, setTierRules] = useState<{
    currency_code: string
    min_purchase_value: number
  }[]>([])

  const form = useForm<EditTierFormData>({
    defaultValues: {
      name: "",
      promo_id: null,
      tier_rules: [],
    },
  })

  // TODO add queries and mutations
}
```

You define a component that receives the tier to edit.

In the component, you define the form and the necessary variables.

Next, you'll add queries to retrieve promotions and store data, and a mutation to update the tier. Replace the `TODO` with the following:

```tsx title="src/admin/components/edit-tier-drawer.tsx"
const { data: promotionsData } = useQuery({
  queryFn: () => sdk.admin.promotion.list(),
  queryKey: ["promotions", "list"],
  enabled: open,
})

const { data: storeData } = useQuery({
  queryFn: () =>
    sdk.admin.store.list({
      fields: "id,supported_currencies.*,supported_currencies.currency.*",
    }),
  queryKey: ["store"],
  enabled: open,
})

const updateTierMutation = useMutation({
  mutationFn: async (data: EditTierFormData) => {
    if (!tier) {return}
    return await sdk.client.fetch(`/admin/tiers/${tier.id}`, {
      method: "POST",
      body: data,
    })
  },
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ["tier", tier?.id] })
    queryClient.invalidateQueries({ queryKey: ["tiers"] })
    setOpen(false)
    toast.success("Success", {
      description: "Tier updated successfully",
      position: "top-right",
    })
  },
  onError: (error) => {
    toast.error("Error", {
      description: error.message,
      position: "top-right",
    })
  },
})

// TODO initialize form on component mount
```

You retrieve the promotions and store data to populate the form with promotions and supported currencies for rules.

You also define a mutation to update a tier using the `useMutation` hook from Tanstack Query.

Next, you'll reset form data when the drawer is opened or closed. Replace the `TODO` with the following:

```tsx title="src/admin/components/edit-tier-drawer.tsx"
useEffect(() => {
  if (tier && open) {
    form.reset({
      name: tier.name,
      promo_id: tier.promotion?.id || null,
      tier_rules: tier.tier_rules || [],
    })
    setTierRules(
      tier.tier_rules?.map((rule) => ({
        currency_code: rule.currency_code,
        min_purchase_value: rule.min_purchase_value,
      })) || []
    )
  }
}, [tier, open, form])

// TODO add function handlers
```

You reset the form data when the drawer is opened.

Next, you'll add functions to handle form submissions and other actions. Replace the `TODO` with the following:

```tsx title="src/admin/components/edit-tier-drawer.tsx"
const handleSubmit = form.handleSubmit((data) => {
  updateTierMutation.mutate({
    ...data,
    tier_rules: tierRules,
  })
})

const promotions = promotionsData?.promotions || []
const store = storeData?.stores?.[0]
const supportedCurrencies = store?.supported_currencies || []

const getAvailableCurrencies = () => {
  const usedCurrencies = new Set(tierRules.map((rule) => rule.currency_code))
  return supportedCurrencies.filter((sc) => !usedCurrencies.has(sc.currency_code))
}

const addTierRule = () => {
  const availableCurrencies = getAvailableCurrencies()
  if (availableCurrencies.length > 0) {
    const firstCurrency = availableCurrencies[0].currency_code
    setTierRules([
      ...tierRules,
      {
        currency_code: firstCurrency,
        min_purchase_value: 0,
      },
    ])
  }
}

const removeTierRule = (index: number) => {
  setTierRules(tierRules.filter((_, i) => i !== index))
}

const updateTierRule = (
  index: number,
  field: "currency_code" | "min_purchase_value",
  value: string | number
) => {
  const updated = [...tierRules]
  updated[index] = {
    ...updated[index],
    [field]: value,
  }
  setTierRules(updated)
}

// TODO add return statement
```

You define the following functions:

- `handleSubmit`: Handles form submissions by calling the `updateTierMutation` mutation with the form data and tier rules.
- `getAvailableCurrencies`: Returns the available currencies that haven't been used yet in the tier rules.
- `addTierRule`: Adds a new tier rule to the form.
- `removeTierRule`: Removes a tier rule from the form.
- `updateTierRule`: Updates a tier rule in the form.

Finally, replace the `TODO` with the following return statement to render the drawer:

```tsx title="src/admin/components/edit-tier-drawer.tsx"
return (
  <Drawer open={open} onOpenChange={setOpen}>
    <Drawer.Trigger asChild>
      <Button variant="secondary" size="small">
        Edit
      </Button>
    </Drawer.Trigger>
    <Drawer.Content>
      <FormProvider {...form}>
        <form onSubmit={handleSubmit} className="flex flex-1 flex-col overflow-hidden">
          <Drawer.Header>
            <Heading level="h1">Edit Tier</Heading>
          </Drawer.Header>
          <Drawer.Body className="flex max-w-full flex-1 flex-col gap-y-8 overflow-y-auto">
            <Controller
              control={form.control}
              name="name"
              rules={{ required: "Name is required" }}
              render={({ field }) => (
                <div className="flex flex-col space-y-2">
                  <Label size="small" weight="plus">
                    Name
                  </Label>
                  <Input {...field} placeholder="e.g., Bronze, Silver, Gold" />
                </div>
              )}
            />

            <Controller
              control={form.control}
              name="promo_id"
              render={({ field }) => (
                <div className="flex flex-col space-y-2">
                  <Label size="small" weight="plus">
                    Promotion (Optional)
                  </Label>
                  <Select
                    value={field.value || ""}
                    onValueChange={(value) => field.onChange(value || null)}
                  >
                    <Select.Trigger>
                      <Select.Value placeholder="Select a promotion" />
                    </Select.Trigger>
                    <Select.Content>
                      {promotions.map((promo) => (
                        <Select.Item key={promo.id} value={promo.id}>
                          {promo.code}
                        </Select.Item>
                      ))}
                    </Select.Content>
                  </Select>
                </div>
              )}
            />

            <div className="flex flex-col gap-y-4">
              <div className="flex items-center justify-between">
                <Label size="small" weight="plus">
                  Tier Rules
                </Label>
                <Button
                  type="button"
                  variant="secondary"
                  size="small"
                  onClick={addTierRule}
                  disabled={getAvailableCurrencies().length === 0}
                >
                  Add Rule
                </Button>
              </div>

              {tierRules.length === 0 && (
                <div className="text-sm text-gray-500">
                  No tier rules added. Click "Add Rule" to add a rule for a currency.
                </div>
              )}

              {tierRules.map((rule, index) => (
                <div key={index} className="flex items-end gap-x-2 rounded-lg border p-4">
                  <div className="flex flex-1 flex-col gap-y-2">
                    <Label size="small">Currency</Label>
                    <Select
                      value={rule.currency_code}
                      onValueChange={(value) => updateTierRule(index, "currency_code", value)}
                    >
                      <Select.Trigger>
                        <Select.Value />
                      </Select.Trigger>
                      <Select.Content>
                        {supportedCurrencies
                          .filter((sc) => {
                            return (
                              sc.currency_code === rule.currency_code ||
                              !tierRules.some(
                                (r, i) => i !== index && r.currency_code === sc.currency_code
                              )
                            )
                          })
                          .map((sc) => (
                            <Select.Item key={sc.currency_code} value={sc.currency_code}>
                              {sc.currency.code.toUpperCase()} - {sc.currency.name}
                            </Select.Item>
                          ))}
                      </Select.Content>
                    </Select>
                  </div>
                  <div className="flex flex-1 flex-col gap-y-2">
                    <Label size="small">Minimum Purchase Value</Label>
                    <Input
                      type="number"
                      min="0"
                      step="0.01"
                      value={rule.min_purchase_value}
                      onChange={(e) =>
                        updateTierRule(index, "min_purchase_value", parseFloat(e.target.value) || 0)
                      }
                    />
                  </div>
                  <IconButton
                    type="button"
                    variant="transparent"
                    size="small"
                    onClick={() => removeTierRule(index)}
                  >
                    <Trash />
                  </IconButton>
                </div>
              ))}
            </div>
          </Drawer.Body>
          <Drawer.Footer>
            <div className="flex items-center justify-end gap-x-2">
              <Drawer.Close asChild>
                <Button size="small" variant="secondary">
                  Cancel
                </Button>
              </Drawer.Close>
              <Button size="small" type="submit" isLoading={updateTierMutation.isPending}>
                Save
              </Button>
            </div>
          </Drawer.Footer>
        </form>
      </FormProvider>
    </Drawer.Content>
  </Drawer>
)
```

You display a `Drawer` from Medusa UI. In the drawer, you render a form with the following fields:

1. **Name**: The name of the tier.
2. **Promotion**: A select input to choose a promotion that's associated with the tier.
3. **Tier Rules**: A list of inputs to specify the minimum purchase value required in a specific currency to qualify for the tier.

### b. Tier Details Section Component

Next, you'll create a component that displays the details of a tier, with a button to open the edit tier drawer.

To create the component, create the file `src/admin/components/tier-details-section.tsx` with the following content:

```tsx title="src/admin/components/tier-details-section.tsx"
import { Code, Container, Heading, Text } from "@medusajs/ui"
import { Link } from "react-router-dom"
import { Tier } from "../routes/tiers/page"
import { EditTierDrawer } from "./edit-tier-drawer"

type TierDetailsSectionProps = {
  tier: Tier | undefined
}

export const TierDetailsSection = ({ tier }: TierDetailsSectionProps) => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h1">Tier Details</Heading>
        <div className="flex items-center gap-x-2">
          <EditTierDrawer tier={tier} />
        </div>
      </div>
      <div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
        <Text size="small" weight="plus" leading="compact">
          Name
        </Text>

        <Text
          size="small"
          leading="compact"
          className="whitespace-pre-line text-pretty"
        >
          {tier?.name ?? "-"}
        </Text>
      </div>
      <div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
        <Text size="small" weight="plus" leading="compact">
          Promotion
        </Text>

        {tier?.promotion && (
          <Link to={`/promotions/${tier.promotion.id}`}>
            <Code>{tier.promotion.code}</Code>
          </Link>
        )}
      </div>
    </Container>
  )
}
```

You display the tier's name and a link to its associated promotion. You also display a button to open the edit tier drawer.

### c. Tier Rules Table Component

Next, you'll create a component that displays the tier rules in a table.

To create the component, create the file `src/admin/components/tier-rules-table.tsx` with the following content:

```tsx title="src/admin/components/tier-rules-table.tsx"
import { Heading, DataTable, createDataTableColumnHelper, useDataTable, Container } from "@medusajs/ui"
import { Tier } from "../routes/tiers/page"

type TierRulesTableProps = {
  tierRules: Tier["tier_rules"] | undefined
}

type TierRule = {
  id: string
  currency_code: string
  min_purchase_value: number
}

const columnHelper = createDataTableColumnHelper<TierRule>()

const columns = [
  columnHelper.accessor("currency_code", {
    header: "Currency",
    cell: ({ getValue }) => getValue().toUpperCase(),
  }),
  columnHelper.accessor("min_purchase_value", {
    header: "Minimum Purchase Value",
  }),
]

export const TierRulesTable = ({ tierRules }: TierRulesTableProps) => {
  const rules = tierRules || []

  const table = useDataTable({
    columns,
    data: rules,
    getRowId: (rule) => rule.id,
    rowCount: rules.length,
    isLoading: false,
  })

  return (
    <Container className="divide-y p-0">
      <DataTable instance={table}>
        <DataTable.Toolbar className="flex items-center justify-between px-6 py-4">
          <Heading level="h2">
            Tier Rules
          </Heading>
        </DataTable.Toolbar>
        <DataTable.Table />
      </DataTable>
    </Container>
  )
}
```

The component receives the tier rules to display.

In the component, you display the tier rules in a `DataTable`. The table shows the currency code and the minimum purchase value required to qualify for the tier.

### d. Tier Customers Table Component

Next, you'll create a component that displays the customers in a tier in a table.

To create the component, create the file `src/admin/components/tier-customers-table.tsx` with the following content:

```tsx title="src/admin/components/tier-customers-table.tsx"
import { Heading, DataTable, createDataTableColumnHelper, useDataTable, Container, DataTablePaginationState } from "@medusajs/ui"
import { sdk } from "../lib/sdk"
import { useQuery } from "@tanstack/react-query"
import { useMemo, useState } from "react"

type TierCustomersTableProps = {
  tierId: string
}

type Customer = {
  id: string
  email: string
  first_name: string | null
  last_name: string | null
}

type CustomersResponse = {
  customers: Customer[]
  count: number
  offset: number
  limit: number
}

const columnHelper = createDataTableColumnHelper<Customer>()

const columns = [
  columnHelper.accessor("email", {
    header: "Email",
  }),
  columnHelper.accessor("first_name", {
    header: "Name",
    cell: ({ row }) => {
      const customer = row.original
      return customer.first_name || customer.last_name
        ? `${customer.first_name || ""} ${customer.last_name || ""}`.trim()
        : "-"
    },
  }),
]

export const TierCustomersTable = ({ tierId }: TierCustomersTableProps) => {
  const limit = 15
  const [pagination, setPagination] = useState<DataTablePaginationState>({
    pageSize: limit,
    pageIndex: 0,
  })

  const offset = useMemo(() => {
    return pagination.pageIndex * limit
  }, [pagination])

  const { data: customersData, isLoading: customersLoading } = useQuery({
    queryFn: () =>
      sdk.client.fetch<CustomersResponse>(`/admin/tiers/${tierId}/customers`, {
        method: "GET",
        query: {
          limit,
          offset,
        },
      }),
    queryKey: ["tier", tierId, "customers"],
    enabled: !!tierId,
  })
  const table = useDataTable({
    columns,
    data: customersData?.customers || [],
    getRowId: (customer) => customer.id,
    rowCount: customersData?.count || 0,
    isLoading: customersLoading,
    pagination: {
      state: pagination,
      onPaginationChange: setPagination,
    },
  })

  return (
    <Container className="divide-y p-0">
      <DataTable instance={table}>
        <DataTable.Toolbar className="flex items-center justify-between px-6 py-4">
          <Heading level="h2">
            Customers in this Tier
          </Heading>
        </DataTable.Toolbar>
        <DataTable.Table />
        <DataTable.Pagination />
      </DataTable>
    </Container>
  )
}
```

The component receives the tier ID to retrieve the customers.

In the component, you fetch the customers in the tier using the API route you created in the previous step.

You display the customers in a `DataTable`. The table shows the email and the name of the customers with pagination controls.

### e. Tier Details UI Route

Finally, you'll create the UI route that displays the details of a tier.

To create the UI route, create the file `src/admin/routes/tiers/[id]/page.tsx` with the following content:

```tsx title="src/admin/routes/tiers/[id]/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { useParams } from "react-router-dom"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../../../lib/sdk"
import { Tier } from "../page"
import { TierDetailsSection } from "../../../components/tier-details-section"
import { TierRulesTable } from "../../../components/tier-rules-table"
import { TierCustomersTable } from "../../../components/tier-customers-table"

type TierResponse = {
  tier: Tier
}

const TierDetailsPage = () => {
  const { id } = useParams()

  const { data: tierData } = useQuery({
    queryFn: () =>
      sdk.client.fetch<TierResponse>(`/admin/tiers/${id}`, {
        method: "GET",
      }),
    queryKey: ["tier", id],
    enabled: !!id,
  })

  const tier = tierData?.tier

  return (
    <>
      <TierDetailsSection tier={tier} />
      <TierRulesTable tierRules={tier?.tier_rules} />
      {tier?.id && <TierCustomersTable tierId={tier.id} />}
    </>
  )
}

export const config = defineRouteConfig({
  label: "Tier Details",
})

export default TierDetailsPage
```

The component retrieves the tier details using the API route you created in the previous step.

Then, you display the components of the different sections you created earlier.

### f. Navigate to Tier Details Page

Next, you'll navigate to the tier details page when the admin user clicks on a row in the tiers list, and after creating a tier.

In `src/admin/routes/tiers/page.tsx`, find the `onRowClick` handler and replace it with the following:

```tsx title="src/admin/routes/tiers/page.tsx"
onRowClick: (_event, row) => {
  navigate(`/tiers/${row.id}`)
}
```

You navigate to the tier details page when the user clicks on a tier in the tiers list.

Next, you'll navigate to the tier details page after creating a tier.

In `src/admin/components/create-tier-modal.tsx`, find the `TODO navigate to the new tier page` comment and replace it with the following:

```tsx title="src/admin/components/create-tier-modal.tsx"
navigate(`/tiers/${data.tier.id}`)
```

You navigate to the tier details page after creating a tier.

### Test Customer Tiers in Medusa Admin

To test out the customer tiers in the Medusa Admin, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and log in using the credentials you set up earlier.

You'll find a new sidebar item labeled "Customer Tiers." Click on it to view the list of tiers.

Click on the tier you created earlier. This will open its details page where you can view the tier's details, its rules, and its customers.

![Tier details page showing tier details, rules, and customers](https://res.cloudinary.com/dza7lstvk/image/upload/v1764067752/Medusa%20Resources/CleanShot_2025-11-25_at_12.48.45_2x_ahjflr.png)

To edit the tier's details, click the Edit button. This will open a drawer where you can edit the tier's name, its associated promotion, and its tier rules.

![Edit tier drawer showing form to edit tier details](https://res.cloudinary.com/dza7lstvk/image/upload/v1764068307/Medusa%20Resources/CleanShot_2025-11-25_at_12.58.17_2x_me8gbo.png)

***

## Step 11: Update Customer Tier on Order

In this step, you'll add the logic to update a customer's tier when an order is placed. This requires creating:

- A method in the Tier Module's service to determine the qualifying tier based on a customer's purchase history.
- A workflow to determine a customer's tier based on their purchase history.
- A [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that listens to the order placement event and executes the workflow.

### a. Determine Qualifying Tier Method

To determine the qualifying tier based on the customer's purchase history, you'll add a method to the Tier Module's service.

In `src/modules/tier/service.ts`, add the following method to the `TierModuleService` class:

```ts title="src/modules/tier/service.ts"
class TierModuleService extends MedusaService({
  Tier,
  TierRule,
}) {
  async calculateQualifyingTier(
    currencyCode: string,
    purchaseValue: number
  ) {
    const rules = await this.listTierRules(
      {
        currency_code: currencyCode,
      }
    )

    if (!rules || rules.length === 0) {
      return null
    }

    const sortedRules = rules.sort(
      (a, b) => b.min_purchase_value - a.min_purchase_value
    )

    const qualifyingRule = sortedRules.find(
      (rule) => purchaseValue >= rule.min_purchase_value
    )

    return qualifyingRule?.tier?.id || null
  }
}
```

The `calculateQualifyingTier` method receives the currency code and the purchase value of a customer.

In the method, you:

- Retrieve the tier rules for the given currency code.
- Sort the rules by the minimum purchase value in ascending order.
- Find the tier whose minimum purchase value is less than or equal to the purchase value.
- Return the ID of the qualifying tier.

You'll use this method in the steps of the workflow to update the customer's tier.

### b. Update Customer Tier on Order Workflow

The workflow to update a customer's tier on order placement has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the details of the order with its customer and tier.
- [validateCustomerStep](#validateCustomerStep): Validate that the customer is a registered customer.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the completed orders for the customer in the same currency.
- [determineTierStep](#determineTierStep): Determine the appropriate tier.

You only need to create the `validateCustomerStep` and `determineTierStep` steps. Medusa provides the other steps out of the box.

#### Validate Customer Step

The `validateCustomerStep` validates that the customer is a registered customer.

To create the step, create the file `src/workflows/steps/validate-customer.ts` with the following content:

```ts title="src/workflows/steps/validate-customer.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { MedusaError } from "@medusajs/framework/utils"

export type ValidateCustomerStepInput = {
  customer: any
}

export const validateCustomerStep = createStep(
  "validate-customer",
  async (input: ValidateCustomerStepInput, { container }) => {
    if (!input.customer) {
      throw new MedusaError(
        MedusaError.Types.NOT_FOUND,
        "Customer not found"
      )
    }

    if (!input.customer.has_account) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Customer must be registered to be assigned a tier"
      )
    }

    return new StepResponse(input.customer)
  }
)
```

The step receives the customer to validate.

In the step, you validate that the customer is defined and that it's registered based on its `has_account` property. Otherwise, you throw an error.

#### Determine Tier Step

The `determineTierStep` determines the appropriate tier based on the customer's purchase history.

To create the step, create the file `src/workflows/steps/determine-tier.ts` with the following content:

```ts title="src/workflows/steps/determine-tier.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { TIER_MODULE } from "../../modules/tier"
import TierModuleService from "../../modules/tier/service"

export type DetermineTierStepInput = {
  currency_code: string
  purchase_value: number
}

export const determineTierStep = createStep(
  "determine-tier",
  async (input: DetermineTierStepInput, { container }) => {
    const tierModuleService: TierModuleService = container.resolve(TIER_MODULE)

    const qualifyingTier = await tierModuleService.calculateQualifyingTier(
      input.currency_code,
      input.purchase_value
    )

    return new StepResponse(qualifyingTier)
  }
)
```

The step receives the currency code and the purchase value of a customer.

In the step, you resolve the Tier Module's service from the Medusa container and call the `calculateQualifyingTier` method to determine the qualifying tier.

You return the ID of the qualifying tier.

#### Update Customer Tier on Order Workflow

You can create the workflow that updates a customer's tier on order placement.

To create the workflow, create the file `src/workflows/update-customer-tier-on-order.ts` with the following content:

```ts title="src/workflows/update-customer-tier-on-order.ts" collapsibleLines="1-16" expandButtonLabel="Show Imports"
import {
  createWorkflow,
  WorkflowResponse,
  transform,
  when,
} from "@medusajs/framework/workflows-sdk"
import {
  useQueryGraphStep,
  createRemoteLinkStep,
  dismissRemoteLinkStep,
} from "@medusajs/medusa/core-flows"
import { Modules, OrderStatus } from "@medusajs/framework/utils"
import { validateCustomerStep } from "./steps/validate-customer"
import { determineTierStep } from "./steps/determine-tier"
import { TIER_MODULE } from "../modules/tier"

type WorkflowInput = {
  order_id: string
}

export const updateCustomerTierOnOrderWorkflow = createWorkflow(
  "update-customer-tier-on-order",
  (input: WorkflowInput) => {
    // Get order details
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: ["id", "currency_code", "total", "customer.*", "customer.tier.*"],
      filters: {
        id: input.order_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const validatedCustomer = validateCustomerStep({
      customer: orders[0].customer,
    })

    // Query completed orders for the customer in the same currency
    const { data: completedOrders } = useQueryGraphStep({
      entity: "order",
      fields: ["id", "total", "currency_code"],
      filters: {
        customer_id: validatedCustomer.id,
        currency_code: orders[0].currency_code,
        status: {
          $nin: [
            OrderStatus.CANCELED,
            OrderStatus.DRAFT,
          ],
        },
      },
    }).config({ name: "completed-orders" })

    // Calculate total purchase value using transform
    const purchasedValue = transform(
      { completedOrders },
      (data) => {
        return data.completedOrders.reduce(
          (sum: number, order: any) => sum + (order.total || 0),
          0
        )
      }
    )

    // Determine appropriate tier
    const tierId = determineTierStep({
      currency_code: orders[0].currency_code as string,
      purchase_value: purchasedValue,
    })

    // Dismiss existing tier link if it exists
    // and the tier id is not the same as the tier id in the determine tier step
    when({ orders, tierId }, (data) => !!data.orders[0].customer?.tier?.id && data.tierId !== data.orders[0].customer?.tier?.id).then(
      () => {
        dismissRemoteLinkStep([
          {
            [TIER_MODULE]: { tier_id: orders[0].customer?.tier?.id as string },
            [Modules.CUSTOMER]: { customer_id: validatedCustomer.id },
          },
        ])
      }
    )

    // Create new tier link if tierId is provided
    when({ tierId, orders }, (data) => !!data.tierId && data.orders[0].customer?.tier?.id !== data.tierId).then(() => {
      createRemoteLinkStep([
        {
          [TIER_MODULE]: { tier_id: tierId },
          [Modules.CUSTOMER]: { customer_id: validatedCustomer.id },
        },
      ])
    })

    return new WorkflowResponse({
      customer_id: validatedCustomer.id,
      tier_id: tierId,
    })
  }
)
```

The workflow receives the order ID as input.

In the workflow, you:

- Retrieve the order details using the `useQueryGraphStep`.
- Validate the customer using the `validateCustomerStep`. This will throw an error if the customer is not a registered customer, which will stop the workflow's execution.
- Retrieve the customer's completed orders in the same currency using `useQueryGraphStep`.
- Calculate the total purchase value using `transform`.
- Determine the appropriate tier using `determineTierStep`.
- Dismiss the existing tier link if it exists and the customer's tier has changed, using `dismissRemoteLinkStep`.
- Create a new tier link if the customer's tier has changed and a new tier ID is provided, using `createRemoteLinkStep`.

Finally, you return a `WorkflowResponse` with the customer ID and the tier ID.

### c. Update Customer Tier on Order Subscriber

Next, you'll create a subscriber that listens to the order placement event and executes the workflow.

A subscriber is an asynchronous function that runs in the background when specific events are emitted.

To create the subscriber, create the file `src/subscribers/order-placed.ts` with the following content:

```ts title="src/subscribers/order-placed.ts"
import {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { updateCustomerTierOnOrderWorkflow } from "../workflows/update-customer-tier-on-order"

export default async function orderPlacedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const logger = container.resolve("logger")
  try {
    await updateCustomerTierOnOrderWorkflow(container).run({
      input: {
        order_id: data.id,
      },
    })
  } catch (error) {
    logger.error(error)
  }
}

export const config: SubscriberConfig = {
  event: "order.placed",
}
```

A subscriber file must export:

- An asynchronous subscriber function that executes whenever the associated event is triggered.
- A configuration object with an event property whose value is the event the subscriber is listening to, which is `order.placed` in this case.

The subscriber function receives an object with the following properties:

- `event`: An object holding the event's details. It has a `data` property, which is the event's data payload.
- `container`: The Medusa container. Use it to resolve modules' main services and other registered resources.

In the subscriber function, you resolve the logger from the Medusa container and execute the workflow. If an error occurs, you log it.

### Test Update Customer Tier on Order

To test the workflow and subscriber, you'll need to place an order using the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) that you installed in the first step.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-customer-tiers`, you can find the storefront by going back to the parent directory and changing to the `medusa-customer-tiers-storefront` directory:

```bash
cd ../medusa-customer-tiers-storefront # change based on your project name
```

First, start the Medusa application by running the following command in the Medusa application's directory:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

Then, start the Next.js Starter Storefront by running the following command in the storefront's directory:

```bash npm2yarn badgeLabel="Next.js Starter Storefront" badgeColor="blue"
npm run dev
```

Next:

1. Open the Next.js Starter Storefront in your browser at `http://localhost:8000`.
2. Click on "Account" in the navigation bar and create a new account.
3. After you're logged in, add products to the cart and complete the checkout process.
   - Make sure the order total is high enough to qualify for the next tier.
4. After you place the order, open the Medusa Admin dashboard at `http://localhost:9000/app` and log in.
5. Go to the Customer Tiers page and click on the tier that the customer should have been assigned to.
6. You'll see the customer in the tier's customers list section.

![Customer in tier's customers list section](https://res.cloudinary.com/dza7lstvk/image/upload/v1764070515/Medusa%20Resources/CleanShot_2025-11-25_at_13.34.32_2x_wro2yk.png)

***

## Step 12: Apply Tier Promotion to Customer Carts

In this step, you'll apply a customer's tier promotion whenever they update their cart if it's not already applied.

To build this feature, you need a workflow that applies the tier promotion to a cart and a subscriber that listens to the cart update event and executes the workflow.

### a. Add Tier Promotion to Cart Workflow

The workflow to add a customer's tier promotion to a cart has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the details of the cart with its customer and tier.
- [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md): Acquire a lock on the cart to prevent concurrent modifications.
- [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md): Release the lock on the cart.

You only need to create the `validateTierPromotionStep`. Medusa provides the other steps and workflows out-of-the-box.

#### Validate Tier Promotion Step

The `validateTierPromotionStep` validates that the customer is registered and has a tier promotion.

To create the step, create the file `src/workflows/steps/validate-tier-promotion.ts` with the following content:

```ts title="src/workflows/steps/validate-tier-promotion.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

export type ValidateTierPromotionStepInput = {
  customer: {
    has_account: boolean
    tier?: {
      promo_id?: string | null
      promotion?: {
        id?: string
        code?: string | null
        status?: string | null
      } | null
    } | null
  } | null
}

export const validateTierPromotionStep = createStep(
  "validate-tier-promotion",
  async (input: ValidateTierPromotionStepInput) => {
    if (!input.customer || !input.customer.has_account) {
      return new StepResponse(null)
    }

    const tier = input.customer.tier

    if (!tier?.promo_id || !tier.promotion || tier.promotion.status !== "active") {
      return new StepResponse({ promotion_code: null })
    }

    return new StepResponse({
      promotion_code: tier.promotion.code || null,
    })
  }
)
```

The step receives the customer to validate.

In the step, you return `null` if the customer is not registered or if it doesn't have a tier promotion. Otherwise, you return the promotion code.

#### Add Tier Promotion to Cart Workflow

You can now create the workflow that adds a customer's tier promotion to a cart.

To create the workflow, create the file `src/workflows/add-tier-promotion-to-cart.ts` with the following content:

```ts title="src/workflows/add-tier-promotion-to-cart.ts" collapsibleLines="1-15" expandButtonLabel="Show Imports"
import {
  createWorkflow,
  WorkflowResponse,
  transform,
  when,
} from "@medusajs/framework/workflows-sdk"
import { 
  acquireLockStep,
  releaseLockStep,
  updateCartPromotionsWorkflow, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { PromotionActions } from "@medusajs/framework/utils"
import { validateTierPromotionStep } from "./steps/validate-tier-promotion"

export type AddTierPromotionToCartWorkflowInput = {
  cart_id: string
}

export const addTierPromotionToCartWorkflow = createWorkflow(
  "add-tier-promotion-to-cart",
  (input: AddTierPromotionToCartWorkflowInput) => {
    // Get cart with customer, tier, and promotions
    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      fields: [
        "id",
        "customer.id",
        "customer.has_account",
        "customer.tier.*",
        "customer.tier.promotion.id",
        "customer.tier.promotion.code",
        "customer.tier.promotion.status",
        "promotions.*",
        "promotions.code",
      ],
      filters: {
        id: input.cart_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    acquireLockStep({
      key: input.cart_id,
      timeout: 2,
      ttl: 10,
    })


    // Check if customer exists and has tier
    const validationResult = when({ carts }, (data) => !!data.carts[0].customer).then(() => {
      return validateTierPromotionStep({
        customer: {
          has_account: carts[0].customer!.has_account,
          tier: {
            promo_id: carts[0].customer!.tier!.promo_id || null,
            promotion: {
              id: carts[0].customer!.tier!.promotion!.id,
              code: carts[0].customer!.tier!.promotion!.code || null,
              // @ts-ignore
              status: carts[0].customer!.tier!.promotion!.status || null,
            },
          },
        },
      })
    })

    // Add promotion to cart if valid and not already applied
    when({ validationResult, carts }, (data) => {
      if (!data.validationResult?.promotion_code) {
        return false
      }

      const appliedPromotionCodes = data.carts[0].promotions?.map(
        (promo: any) => promo.code
      ) || []

      return (
        data.validationResult?.promotion_code !== null &&
        !appliedPromotionCodes.includes(data.validationResult?.promotion_code!)
      )
    }).then(() => {
      return updateCartPromotionsWorkflow.runAsStep({
        input: {
          cart_id: input.cart_id,
          promo_codes: [validationResult?.promotion_code!],
          action: PromotionActions.ADD,
        },
      })
    })

    releaseLockStep({
      key: input.cart_id,
    })

    return new WorkflowResponse(void 0)
  }
)
```

The workflow receives the cart's ID as input.

In the workflow, you:

- Retrieve the cart details using `useQueryGraphStep`.
- Acquire a lock on the cart using `acquireLockStep`.
- Validate that the customer exists and has a tier promotion using `validateTierPromotionStep`.
- Update the cart's promotions if the customer has a tier promotion that hasn't been applied yet, using `updateCartPromotionsWorkflow`.
- Release the lock on the cart using `releaseLockStep`.

### b. Cart Updated Subscriber

Next, you'll create a subscriber that listens to the cart update event and executes the workflow.

To create the subscriber, create the file `src/subscribers/cart-updated.ts` with the following content:

```ts title="src/subscribers/cart-updated.ts"
import {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { addTierPromotionToCartWorkflow } from "../workflows/add-tier-promotion-to-cart"

export default async function cartUpdatedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await addTierPromotionToCartWorkflow(container).run({
    input: {
      cart_id: data.id,
    },
  })
}

export const config: SubscriberConfig = {
  event: "cart.updated",
}
```

The subscriber listens to the `cart.updated` event and executes the workflow.

### Test Add Tier Promotion to Cart

To test the automated tier promotion, make sure both the Medusa application and the Next.js Starter Storefront are running.

Then, in the Next.js Starter Storefront, log in as a customer in a tier, and add a product to the cart.

You can see that the discount of the customer's tier is applied to the cart.

If you don't see the promotion applied, try to refresh the page.

![Cart showing the promotion of the tier applied](https://res.cloudinary.com/dza7lstvk/image/upload/v1764072761/Medusa%20Resources/CleanShot_2025-11-25_at_13.47.49_2x_xmewsm.png)

***

## Step 13: Validate Applied Promotion

In this step, you'll validate that a cart's promotions match the customer's tier. You'll apply validation when new promotions are added to the cart, and when completing the cart.

### Validate Promotion Addition

When a customer adds a promotion to the cart, the storefront sends a request to the [Add Promotions API Route](https://docs.medusajs.com/api/store#carts_postcartsidpromotions), which executes the [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartPromotionsWorkflow/index.html.md).

To ensure that customers don't add promotions that belong to different tiers, you can consume the `validate` [hook](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md) of the `updateCartPromotionsWorkflow`. A hook is a specific point in a workflow where you can inject custom functionality.

To consume the hook, create the file `src/workflows/hooks/update-cart-promotions-validate.ts` with the following content:

```ts title="src/workflows/hooks/update-cart-promotions-validate.ts"
import { updateCartPromotionsWorkflow } from "@medusajs/medusa/core-flows"
import { MedusaError } from "@medusajs/framework/utils"
import { PromotionActions } from "@medusajs/framework/utils"

updateCartPromotionsWorkflow.hooks.validate(async ({ input, cart }, { container }) => {
  const query = container.resolve("query")

  // Only validate when adding promotions
  if (
    (input.action !== PromotionActions.ADD && input.action !== PromotionActions.REPLACE) || 
    !input.promo_codes || input.promo_codes.length === 0
  ) {
    return
  }

  // Get customer details with tier
  const data = cart.customer_id ? await query.graph({
    entity: "customer",
    fields: ["id", "tier.*"],
    filters: {
      id: cart.customer_id,
    },
  }) : null

  // Get customer's tier
  const customerTier = data?.data?.[0]?.tier

  // Get promotions by codes to check if they're tier promotions
  const { data: promotions } = await query.graph({
    entity: "promotion",
    fields: ["id", "code"],
    filters: {
      code: input.promo_codes,
    },
  })

  // Get all tiers with their promotion IDs
  const { data: allTiers } = await query.graph({
    entity: "tier",
    fields: ["id", "promo_id"],
    filters: {
      promo_id: promotions.map((p) => p.id),
    },
  })

  // Validate each promotion being added
  for (const promotion of promotions || []) {
    const tierId = allTiers.find((t) => t.promo_id === promotion?.id)?.id
    
    // If this promotion belongs to a tier
    if (tierId && customerTier?.id !== tierId) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          `Promotion ${promotion.code || promotion.id} can only be applied by customers in the corresponding tier.`
        )
    }
  }
})
```

You consume a hook by accessing it through the workflow's `hooks` property. The hook accepts a step function as a parameter.

In the hook, you:

1. Return early if the customer isn't adding a promotion.
2. Retrieve the customer if it's set in the cart.
3. Retrieve promotions being added to the cart.
4. Retrieve all tiers associated with the promotions.
5. Loop over the promotions and check if the customer belongs to the tier associated with the promotion.
   - If not, you throw an error, which will stop the customer from adding the promotion to the cart.

### Test Validate Promotion Addition

To test the promotion addition validation, make sure both the Medusa application and the Next.js Starter Storefront are running.

Then, in the Next.js Starter Storefront, log in as a customer in a tier, and add a product to the cart.

Try to add a promotion that belongs to a different tier. You should see an error message.

![Error message when trying to add a promotion of a different tier to the cart](https://res.cloudinary.com/dza7lstvk/image/upload/v1764074555/Medusa%20Resources/CleanShot_2025-11-25_at_14.42.18_2x_uxkfgk.png)

### Validate Cart Completion

Next, you'll add similar validation for the cart's promotions when completing the cart. You'll perform the validation by consuming the `validate` hook of the `completeCartWorkflow`.

Create the file `src/workflows/hooks/complete-cart-validate.ts` with the following content:

```ts title="src/workflows/hooks/complete-cart-validate.ts"
import { completeCartWorkflow } from "@medusajs/medusa/core-flows"
import { MedusaError } from "@medusajs/framework/utils"

completeCartWorkflow.hooks.validate(async ({ cart }, { container }) => {
  const query = container.resolve("query")

  // Get cart with promotions
  const { data: [detailedCart] } = await query.graph({
    entity: "cart",
    fields: ["id", "promotions.*", "customer.id", "customer.tier.*"],
    filters: {
      id: cart.id,
    },
  }, {
    throwIfKeyNotFound: true,
  })

  if (!detailedCart?.promotions || detailedCart.promotions.length === 0) {
    return
  }

  // Get customer's tier
  const customerTier = detailedCart.customer?.tier

  // Get all tier promotions to check
  const { data: allTiers } = await query.graph({
    entity: "tier",
    fields: ["id", "promo_id"],
    filters: {
      promo_id: detailedCart.promotions.map((p) => p?.id).filter(Boolean) as string[],
    },
  })

  // Validate that if a tier promotion is applied, the customer belongs to that tier
  for (const promotion of detailedCart.promotions) {
    const tierId = allTiers.find((t) => t.promo_id === promotion?.id)?.id
    if (tierId && customerTier?.id !== tierId) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        `Promotion ${promotion?.code || promotion?.id} can only be applied by customers in the corresponding tier.`
      )
    }
  }
})
```

Similar to the previous hook, you:

1. Retrieve the cart with its promotions and customer tier.
   - If the cart doesn't have promotions, return early.
2. Retrieve the tiers associated with the applied promotions.
3. Loop over the promotions and check if any promotion doesn't belong to the customer's tier.
   - If so, you throw an error, which will stop the customer from completing the cart.

This validation will run every time the cart is completed and before the order is placed.

***

## Step 14: Show Customer Tier in Storefront

In this step, you'll add an API route that retrieves a customer's current tier and their next tier. Then, you'll customize the Next.js Starter Storefront to show a tier progress indicator on the account and order confirmation pages.

### a. Calculate Next Tier Method

To calculate the customer's next tier, you'll add a method to the Tier Module's service. You'll then use that method in the API route to retrieve the customer's next tier.

In `src/modules/tier/service.ts`, add the following method to the `TierModuleService` class:

```ts title="src/modules/tier/service.ts"
class TierModuleService extends MedusaService({
  Tier,
  TierRule,
}) {
  // ...
  async calculateNextTierUpgrade(
    currencyCode: string,
    currentPurchaseValue: number
  ) {
    const rules = await this.listTierRules(
      {
        currency_code: currencyCode,
      },
      {
        relations: ["tier"],
      }
    )
    
    // Sort rules by min_purchase_value in ascending orderding order
    const sortedRules = rules.sort(
      (a, b) => a.min_purchase_value - b.min_purchase_value
    )

    // Find the next tier the customer hasn't reached
    const nextRule = sortedRules.find(
      (rule) => rule.min_purchase_value > currentPurchaseValue
    )

    if (!nextRule || !nextRule.tier) {
      return null
    }

    const requiredAmount = nextRule.min_purchase_value - currentPurchaseValue

    return {
      tier: nextRule.tier,
      required_amount: requiredAmount,
      current_purchase_value: currentPurchaseValue,
      next_tier_min_purchase: nextRule.min_purchase_value,
    }
  }
}
```

The `calculateNextTierUpgrade` method receives the currency code and the current purchase value of a customer.

In the method, you:

- Retrieve the tier rules for the given currency code.
- Sort the rules by the minimum purchase value in ascending order.
- Find the next tier the customer hasn't reached.
- Return the next tier, the required amount to reach the next tier, the current purchase value, and the minimum purchase value of the next tier.

### b. Next Tier API Route

Next, you'll create an API route that retrieves a customer's current tier and their next tier.

To create the API route, create the file `src/api/store/customers/me/next-tier/route.ts` with the following content:

```ts title="src/api/store/customers/me/next-tier/route.ts"
import {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { TIER_MODULE } from "../../../../../modules/tier"
import { MedusaError } from "@medusajs/framework/utils"
import { z } from "zod"
import { OrderStatus } from "@medusajs/framework/utils"

export const NextTierSchema = z.object({
  region_id: z.string(),
})

type NextTierInput = z.infer<typeof NextTierSchema>

export async function GET(
  req: AuthenticatedMedusaRequest<{}, NextTierInput>,
  res: MedusaResponse
): Promise<void> {
  // Validate customer is authenticated
  const customerId = req.auth_context?.actor_id

  if (!customerId) {
    throw new MedusaError(
      MedusaError.Types.UNAUTHORIZED,
      "Customer must be authenticated"
    )
  }

  const query = req.scope.resolve("query")
  const tierModuleService = req.scope.resolve(TIER_MODULE)

  // Get customer details to validate they're registered
  const { data: [customer] } = await query.graph({
    entity: "customer",
    fields: ["id", "has_account", "tier.*"],
    filters: {
      id: customerId,
    },
  }, {
    throwIfKeyNotFound: true,
  })

  if (!customer.has_account) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "Customer must be registered to view tier information"
    )
  }

  // Get currency code from cart or region context
  // Try to get from cart first, then region
  const regionId = req.validatedQuery.region_id

  // Calculate total purchase value
  const { data: orders } = await query.graph({
    entity: "order",
    fields: ["id", "total", "currency_code"],
    filters: {
      customer_id: customerId,
      region_id: regionId,
      status: {
        $nin: [
          OrderStatus.CANCELED,
          OrderStatus.DRAFT,
        ],
      },
    },
  })

  // Get currency code from region if no orders
  let currencyCode: string | null = null
  if (orders.length > 0) {
    currencyCode = orders[0].currency_code
  } else {
    // Get currency from region
    const { data: regions } = await query.graph({
      entity: "region",
      fields: ["id", "currency_code"],
      filters: {
        id: regionId,
      },
    })
    
    if (regions && regions.length > 0) {
      currencyCode = regions[0].currency_code
    }
  }

  const totalPurchaseValue = orders.length > 0
    ? orders.reduce((sum: number, order: any) => sum + (order.total || 0), 0)
    : 0

  // Current tier is always the customer's assigned tier (null if not assigned)
  const currentTier = customer.tier || null

  // Determine next tier upgrade
  const nextTierUpgrade = await tierModuleService.calculateNextTierUpgrade(
    currencyCode as string,
    totalPurchaseValue
  )

  res.json({
    current_tier: currentTier,
    current_purchase_value: totalPurchaseValue,
    currency_code: currencyCode,
    next_tier_upgrade: nextTierUpgrade,
  })
}
```

You first define a Zod schema to validate incoming requests. Requests must have a `region_id` query parameter. This determines the currency code to use for the tier calculation.

Then, you export a `GET` function, which exposes a `GET` API route at `/store/customers/me/next-tier`.

In the route handler, you:

- Validate that the customer is authenticated.
- Resolve Query and the Tier Module service from the Medusa container.
- Retrieve the customer details to validate that they're registered.
- Retrieve the currency code from the cart or region context.
- Calculate the total purchase value of the customer.
- Determine the customer's current tier.
- Determine the customer's next tier upgrade.
- Return the customer's current tier, current purchase value, currency code, and next tier upgrade in the response.

### c. Apply Query Validation Middleware

Next, you need to apply a middleware that validates the query parameters passed to the request, and sets the default Query configurations.

In `src/api/middlewares.ts`, add the import at the top of the file:

```ts title="src/api/middlewares.ts"
import { NextTierSchema } from "./store/customers/me/next-tier/route"
```

Then, add the following object to the `routes` array passed to `defineMiddlewares`:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/store/customers/me/next-tier",
      methods: ["GET"],
      middlewares: [validateAndTransformQuery(NextTierSchema, {})],
    },
  ],
})
```

You apply the `validateAndTransformQuery` middleware to the `GET` route of the `/store/customers/me/next-tier` path, passing it the `NextTierSchema` schema to validate the request parameters.

### d. Show Customer Tier in Storefront

Next, you'll customize the Next.js Starter Storefront to show a tier progress indicator in the account and order confirmation pages.

#### Define Tier Types

First, you'll define types for the tier information received from the Medusa server.

Create the file `src/types/tier.ts` in the storefront with the following content:

```ts title="src/types/tier.ts" badgeLabel="Storefront" badgeColor="blue"
export type Tier = {
  id: string
  name: string
  promotion_id: string
}

export type CustomerNextTier = {
  current_tier: Tier | null
  current_purchase_value: number
  currency_code: string
  next_tier_upgrade: {
    tier: Tier | null
    required_amount: number
    current_purchase_value: number
    next_tier_min_purchase: number
  } | null
}
```

The `Tier` type represents a tier, and the `CustomerNextTier` type represents the response received from the `/store/customers/me/next-tier` API route.

#### Retrieve Customer Tier and Next Tier

Next, you'll add a server function that retrieves the customer's current tier and their next tier.

In `src/lib/data/customer.ts`, add the following imports at the top of the file:

```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue"
import { CustomerNextTier } from "types/tier"
import { getRegion } from "./regions"
```

Then, add the following function to the file:

```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue"
export const retrieveCustomerNextTier =
  async (countryCode: string): Promise<CustomerNextTier | null> => {
    const authHeaders = await getAuthHeaders()
    const region = await getRegion(countryCode)

    if (!region) {return null}

    if (!authHeaders) {return null}

    const headers = {
      ...authHeaders,
    }

    const next = {
      ...(await getCacheOptions("customers")),
    }

    return await sdk.client
      .fetch<CustomerNextTier>(`/store/customers/me/next-tier`, {
        method: "GET",
        headers,
        next,
        query: {
          region_id: region.id,
        },
      })
      .then((data) => data)
      .catch(() => null)
}
```

You create a function that retrieves the customer's current tier and their next tier from the `/store/customers/me/next-tier` API route.

#### Add Customer Tier Component

Next, you'll create a component that displays a progress indicator for the customer's tier. You'll use this component on the account and order confirmation pages.

Create the file `src/modules/common/customer-tier/index.tsx` with the following content:

```tsx title="src/modules/common/customer-tier/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { convertToLocale } from "@lib/util/money"
import { clx } from "@medusajs/ui"
import { CustomerNextTier } from "types/tier"

type CustomerTierProps = {
  tierData: CustomerNextTier | null
}

const CustomerTier = ({ tierData }: CustomerTierProps) => {
  if (!tierData) {
    return null
  }

  const { current_tier, current_purchase_value, currency_code, next_tier_upgrade } = tierData

  // Calculate progress if there's a next tier
  let progressPercentage = 100
  let amountNeeded = 0
  let minPurchaseValue = 0
  let hasNextTier = false
  let nextTierName = ""

  if (next_tier_upgrade && next_tier_upgrade.tier) {
    hasNextTier = true
    nextTierName = next_tier_upgrade.tier.name
    amountNeeded = next_tier_upgrade.required_amount
    minPurchaseValue = next_tier_upgrade.next_tier_min_purchase
    
    // Calculate progress percentage
    // Use the current purchase value and next tier min purchase from the API
    const currentPurchase = next_tier_upgrade.current_purchase_value
    const nextMin = next_tier_upgrade.next_tier_min_purchase
    
    // Calculate progress: current purchase value / next tier min purchase
    if (nextMin > 0) {
      progressPercentage = Math.min(100, Math.max(0, (currentPurchase / nextMin) * 100))
    } else {
      progressPercentage = 100
    }
  }

  // If no current tier and no next tier, don't show anything
  if (!current_tier && !hasNextTier) {
    return null
  }

  return (
    <div className="flex flex-col gap-y-4">
      <h3 className="text-large-semi">Membership Tier</h3>
      <div className="flex flex-col gap-y-3">
        {current_tier ? (
          <div className="flex items-center gap-x-2">
            <span className="text-large-semi" data-testid="current-tier-name">
              {current_tier.name}
            </span>
          </div>
        ) : (
          <div className="flex items-center gap-x-2">
            <span className="text-large-semi text-ui-fg-subtle" data-testid="current-tier-name">
              No tier
            </span>
          </div>
        )}

        {hasNextTier && (
          <div className="flex flex-col gap-y-2">
            <div className="flex justify-between text-small-regular text-ui-fg-subtle">
              <span>Progress to {nextTierName}</span>
              {amountNeeded > 0 ? (
                <span>
                  {convertToLocale({
                    amount: amountNeeded,
                    currency_code: currency_code,
                  })}{" "}
                  to go
                </span>
              ) : (
                <span className="text-ui-fg-interactive">Threshold reached!</span>
              )}
            </div>
            <div className="flex">
              <div
                className={clx(
                  `h-2 rounded-s-full transition-all duration-500 ease-in-out`,
                  progressPercentage >= 100
                    ? "bg-gradient-to-r from-green-400 to-green-500"
                    : "bg-gradient-to-r from-ui-fg-interactive to-ui-fg-interactive-hover",
                  progressPercentage === 100 && "rounded-e-full"
                )}
                style={{ width: `${progressPercentage}%` }}
                data-testid="tier-progress-bar"
              />
              <div className={clx(
                "bg-gray-200 h-2 rounded-e-full flex-grow",
                progressPercentage === 0 && "rounded-s-full"
              )} />
            </div>
            <div className="flex justify-between text-xs text-ui-fg-subtle">
              <span>
                {convertToLocale({
                  amount: next_tier_upgrade?.current_purchase_value || current_purchase_value,
                  currency_code: currency_code,
                })}
              </span>
              {minPurchaseValue > 0 && (
                <span>
                  {convertToLocale({
                    amount: minPurchaseValue,
                    currency_code: currency_code,
                  })}
                </span>
              )}
            </div>
          </div>
        )}

        {!hasNextTier && current_tier && (
          <div className="text-small-regular text-ui-fg-subtle">
            You&apos;ve reached the highest tier!
          </div>
        )}
      </div>
    </div>
  )
}

export default CustomerTier
```

The component receives the tier data retrieved from the Medusa server as a prop. It then calculates and displays a progress bar indicating how much the customer needs to spend to unlock the next tier.

#### Show Customer Tier on Account Page

Next, you'll show the customer's tier on the account page.

In `src/modules/account/components/overview/index.tsx`, add the following imports at the top of the file:

```tsx title="src/modules/account/components/overview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import CustomerTier from "@modules/common/customer-tier"
import { CustomerNextTier } from "types/tier"
```

Then, update the `Overview` component's props to include the `tierData` prop:

```tsx title="src/modules/account/components/overview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
type OverviewProps = {
  // ...
  tierData: CustomerNextTier | null
}

const Overview = ({ customer, orders, tierData }: OverviewProps) => {
  // ...
}
```

Finally, update the `return` statement to render the `CustomerTier` component before the `div` wrapping the recent orders:

```tsx title="src/modules/account/components/overview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div>
    {/* ... */}
    {tierData && (
      <div className="mb-6">
        <CustomerTier tierData={tierData} />
      </div>
    )}
    {/* ... */}
  </div>
)
```

To pass the tier data to the `Overview` component, replace the content of `src/app/[countryCode]/(main)/account/@dashboard/page.tsx` with the following:

```tsx title="src/app/[countryCode]/(main)/account/@dashboard/page.tsx" badgeLabel="Storefront" badgeColor="blue"
import { Metadata } from "next"

import Overview from "@modules/account/components/overview"
import { notFound } from "next/navigation"
import { retrieveCustomer, retrieveCustomerNextTier } from "@lib/data/customer"
import { listOrders } from "@lib/data/orders"

export const metadata: Metadata = {
  title: "Account",
  description: "Overview of your account activity.",
}

type Props = {
  params: Promise<{ countryCode: string }>
}

export default async function OverviewTemplate(props: Props) {
  const params = await props.params
  const { countryCode } = params
  const customer = await retrieveCustomer().catch(() => null)
  const orders = (await listOrders().catch(() => null)) || null
  const tierData = await retrieveCustomerNextTier(countryCode).catch(() => null)

  if (!customer) {
    notFound()
  }

  return <Overview customer={customer} orders={orders} tierData={tierData} />
}
```

You make the following key changes:

- Add the import for the `retrieveCustomerNextTier` function.
- Add the `countryCode` parameter type to the `OverviewTemplate` component props.
- Retrieve the customer's tier and next tier information using the `retrieveCustomerNextTier` function.
- Pass the tier data to the `Overview` component.

#### Test Customer Tier on Account Page

To test the customer tier component on the account page, make sure both the Medusa server and the Next.js Starter Storefront are running.

Then, on the storefront, click "Account" in the navigation bar. The main account page will display the customer's current tier with a progress bar showing their progress toward the next tier.

![Customer tier component on account page](https://res.cloudinary.com/dza7lstvk/image/upload/v1764078181/Medusa%20Resources/CleanShot_2025-11-25_at_15.42.44_2x_hhguzm.png)

#### Show Customer Tier on Order Confirmation Page

Next, you'll show the customer tier component on the order confirmation page.

In `src/modules/order/templates/order-completed-template.tsx`, add the following imports at the top of the file:

```tsx title="src/modules/order/templates/order-completed-template.tsx" badgeLabel="Storefront" badgeColor="blue"
import CustomerTier from "@modules/common/customer-tier"
import { CustomerNextTier } from "types/tier"
```

Then, update the `OrderCompletedTemplate` component's props to include the `tierData` prop:

```tsx title="src/modules/order/templates/order-completed-template.tsx" badgeLabel="Storefront" badgeColor="blue"
type OrderCompletedTemplateProps = {
  // ...
  tierData: CustomerNextTier | null
}

export default async function OrderCompletedTemplate({
  // ...
  tierData,
}: OrderCompletedTemplateProps) {
  // ...
}
```

Finally, update the `return` statement to render the `CustomerTier` component before the "Summary" heading:

```tsx title="src/modules/order/templates/order-completed-template.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div>
    {/* ... */}
    {tierData && (
      <div className="mt-6">
        <CustomerTier tierData={tierData} />
      </div>
    )}
    {/* ... */}
  </div>
)
```

To pass the tier data to the `OrderCompletedTemplate` component, open `src/app/[countryCode]/(main)/order/[id]/confirmed/page.tsx` and replace the content with the following:

```tsx title="src/app/[countryCode]/(main)/order/[id]/confirmed/page.tsx" badgeLabel="Storefront" badgeColor="blue"
import { retrieveOrder } from "@lib/data/orders"
import OrderCompletedTemplate from "@modules/order/templates/order-completed-template"
import { Metadata } from "next"
import { notFound } from "next/navigation"
import { retrieveCustomerNextTier } from "@lib/data/customer"

type Props = {
  params: Promise<{ id: string; countryCode: string }>
}
export const metadata: Metadata = {
  title: "Order Confirmed",
  description: "You purchase was successful",
}

export default async function OrderConfirmedPage(props: Props) {
  const params = await props.params
  const order = await retrieveOrder(params.id).catch(() => null)
  const tierData = await retrieveCustomerNextTier(params.countryCode).catch(() => null)

  if (!order) {
    return notFound()
  }

  return <OrderCompletedTemplate order={order} tierData={tierData} />
}
```

You make the following key changes:

- Add the import for the `retrieveCustomerNextTier` function.
- Add the `countryCode` parameter to the `OrderConfirmedPage` component.
- Retrieve the customer's tier and their next tier using the `retrieveCustomerNextTier` function.
- Pass the tier data to the `OrderCompletedTemplate` component.

#### Test Customer Tier on Order Confirmation Page

To test the customer tier component on the order confirmation page, make sure both the Medusa server and the Next.js Starter Storefront are running.

Then, on the storefront, place an order. The order confirmation page will display the customer's tier with a progress bar showing their progress toward the next tier.

![Customer tier component on order confirmation page](https://res.cloudinary.com/dza7lstvk/image/upload/v1764078663/Medusa%20Resources/CleanShot_2025-11-25_at_15.50.50_2x_liqi8r.png)

***

## Next Steps

You've now implemented customer tiers in Medusa. You can expand on this feature to add more features like:

- Automated emails to customers when they reach a new tier. Cloud users can benefit from zero-config email setup with [Medusa Emails](https://docs.medusajs.com/cloud/emails/index.html.md).
- More complex tier rules, such as rules based on product categories or collections.
- Other tier privileges, such as early access to new products or free shipping.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md) for a more in-depth understanding of the concepts you've used in this guide and more.

To learn more about the commerce features Medusa provides, check out [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.


# Implement First-Purchase Discount in Medusa

In this tutorial, you'll learn how to implement first-purchase discounts in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md), which are available out-of-the-box. These features include promotion and cart management features.

The first-purchase discount feature encourages customers to sign up and make their first purchase by offering them a discount. In this tutorial, you'll learn how to implement this feature in Medusa.

## Summary

By following this tutorial, you'll learn how to:

- Install and set up Medusa.
- Apply a first-purchase discount to a customer's cart if they are a first-time customer.
- Add custom validation to ensure the discount is only used by first-time customers.
- Customize the Next.js Starter Storefront to display a pop-up encouraging first-time customers to sign up and receive a discount.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Diagram showcasing the flow of first-time purchase discounts](https://res.cloudinary.com/dza7lstvk/image/upload/v1750846212/Medusa%20Resources/first-purchase-promo-overview_jbiwa9.jpg)

[View on Github](https://github.com/medusajs/examples/tree/main/first-purchase-discount): Find the full code for this tutorial.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

First, you'll be asked for the project's name. Then, when prompted about installing the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."

Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create a First-Purchase Promotion

Before you apply the first-purchase discount or promotion to a customer's cart, you need to create the promotion that will be applied.

Start your Medusa application with the following command:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and log in with the user you created in the previous step.

Next, click on the "Promotions" tab in the left sidebar, then click on the "Create Promotion" button to create a new promotion.

You can customize the promotion based on your use case. For example, it can be a `10%` off the entire order, or a fixed amount off specific items.

Make sure to set the promotion's code to `FIRST_PURCHASE`, as you'll be using this code in your Medusa customization. If you want to use a different code, make sure to update the code in the next steps accordingly.

Refer to the [Create Promotions User Guide](https://docs.medusajs.com/user-guide/promotions/create/index.html.md) to learn how to create promotions in Medusa.

Once you create and publish the promotion, you can proceed to the next steps.

![First-purchase promotion in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1750846696/Medusa%20Resources/CleanShot_2025-06-25_at_13.18.00_2x_j46emw.png)

***

## Step 3: Apply the First-Purchase Discount to Cart

In this step, you'll customize the Medusa application to automatically apply the first-purchase promotion to a cart.

To build this feature, you need to:

- Create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) that implements the logic to apply the first-purchase promotion to a cart.
- Execute the workflow in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that is triggered when a cart is created, or when it's transferred to a customer.

### a. Store the First-Purchase Promotion Code

Since you'll refer to the first-purchase promotion code in multiple places, it's a good idea to store it as a constant in your Medusa application.

So, create the file `src/constants.ts` with the following content:

```ts title="src/constants.ts"
export const FIRST_PURCHASE_PROMOTION_CODE = "FIRST_PURCHASE"
```

You'll reference this constant in the next steps.

### b. Create the Workflow

Next, you'll create the workflow that implements the logic to apply the first-purchase promotion to a cart.

A [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) is a series of actions, called steps, that complete a task with rollback and retry mechanisms. In Medusa, you build commerce features in workflows, then execute them in other customizations, such as subscribers, scheduled jobs, and API routes.

The workflow you'll build will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details, including its promotions and customer.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the details of the first-purchase promotion.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the updated cart's details, including its promotions.

Medusa provides all these steps in its `@medusajs/medusa/core-flows` package, so you can implement the workflow right away.

To create the workflow, create the file `src/workflows/apply-first-purchase-promo.ts` with the following content:

```ts title="src/workflows/apply-first-purchase-promo.ts" highlights={workflowHighlights}
import { createWorkflow, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { updateCartPromotionsStep, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { FIRST_PURCHASE_PROMOTION_CODE } from "../constants"
import { PromotionActions } from "@medusajs/framework/utils"

type WorkflowInput = {
  cart_id: string
}

export const applyFirstPurchasePromoWorkflow = createWorkflow(
  "apply-first-purchase-promo",
  (input: WorkflowInput) => {
    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      fields: ["promotions.*", "customer.*", "customer.orders.*"],
      filters: {
        id: input.cart_id,
      },
    })

    const { data: promotions } = useQueryGraphStep({
      entity: "promotion",
      fields: ["code"],
      filters: {
        code: FIRST_PURCHASE_PROMOTION_CODE,
      },
    }).config({ name: "retrieve-promotions" })

    when({ 
      carts,
      promotions,
    }, (data) => {
      return data.promotions.length > 0 && 
        !data.carts[0].promotions?.some((promo) => promo?.id === data.promotions[0].id) && 
        data.carts[0].customer !== null && 
        data.carts[0].customer.orders?.length === 0
    })
    .then(() => {
      updateCartPromotionsStep({
        id: carts[0].id,
        promo_codes: [promotions[0].code!],
        action: PromotionActions.ADD,
      })
    })

    // retrieve updated cart
    const { data: updatedCarts } = useQueryGraphStep({
      entity: "cart",
      fields: ["*", "promotions.*"],
      filters: {
        id: input.cart_id,
      },
    }).config({ name: "retrieve-updated-cart" })

    return new WorkflowResponse(updatedCarts[0])
  }
)
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

`createWorkflow` accepts as a second parameter a constructor function, which is the workflow's implementation. The function accepts as an input an object with the ID of the cart to apply the first-purchase promotion to.

In the workflow's constructor function, you:

- Retrieve the cart's details, including its promotions and customer, using the [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md).
- Retrieve the details of the first-purchase promotion using the `useQueryGraphStep`.
  - You pass the `FIRST_PURCHASE_PROMOTION_CODE` constant to the `filters` option to retrieve the promotion.
- Use the [when-then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) utility to only apply the promotion if the first-purchase promotion exists, the cart doesn't have the promotion, and the customer doesn't have any orders. `when` receives two parameters:
  - An object to use in the condition function.
  - A condition function that receives the first parameter object and returns a boolean indicating whether to execute the steps in the `then` block.
- Retrieve the updated cart's details, including its promotions, using the `useQueryGraphStep` again.

Finally, you return a `WorkflowResponse` with the updated cart's details.

You can't perform data manipulation in a workflow's constructor function. Instead, the Workflows SDK includes utility functions like `when` to perform typical operations that require accessing data values. Learn more about workflow constraints in the [Workflow Constraints](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documentation.

### c. Create the Subscriber

Next, you'll create a subscriber that executes the workflow when a cart is created or transferred to a customer.

A cart can be transferred to a customer when they sign up or log in, or in B2B use cases.

A [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) is an asynchronous function that listens to events to perform a task. In this case, you'll create a subscriber that listens to the `cart.created` and `cart.customer_transferred` events to execute the workflow.

To create the subscriber, create the file `src/subscribers/apply-first-purchase.ts` with the following content:

```ts title="src/subscribers/apply-first-purchase.ts"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { applyFirstPurchasePromoWorkflow } from "../workflows/apply-first-purchase-promo"

export default async function cartCreatedHandler({
  event: { data },
  container,
}: SubscriberArgs<{
  id: string
}>) {
  await applyFirstPurchasePromoWorkflow(container)
  .run({
    input: {
      cart_id: data.id,
    },
  })
}

export const config: SubscriberConfig = {
  event: ["cart.created", "cart.customer_transferred"],
}
```

A subscriber file must export:

1. An asynchronous function, which is the subscriber that is executed when the event is emitted.
2. A configuration object that holds the names of the events the subscriber listens to, which are `cart.created` and `cart.customer_transferred` in this case.

The subscriber function receives an object as a parameter that has a `container` property, which is the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). The Medusa container holds Framework and commerce tools that you can resolve and use in your customizations.

In the subscriber function, you execute the `applyFirstPurchasePromoWorkflow` by invoking it, passing it the Medusa container, then calling its `run` method. You pass the `cart_id` from the event payload as an input to the workflow.

### Test it Out

You can now test the automatic application of the first-purchase promotion to a cart. To do that, you'll use the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) you installed in the first step.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-first-promo`, you can find the storefront by going back to the parent directory and changing to the `medusa-first-promo-storefront` directory:

```bash
cd ../medusa-first-promo-storefront # change based on your project name
```

First, start the Medusa application with the following command:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

Then, start the Next.js Starter Storefront with the following command:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

The storefront will run at `http://localhost:8000`. Open it in your browser and click on Account at the top right to register.

After you register, add a product to the cart, then go to the cart page. You'll find that the `FIRST_PURCHASE` promotion has been applied to the cart automatically.

![Cart page with first-purchase promotion applied](https://res.cloudinary.com/dza7lstvk/image/upload/v1750842319/Medusa%20Resources/CleanShot_2025-06-25_at_12.02.17_2x_bbu8vt.png)

***

## Step 4: Validate First-Purchase Discount Usage

You now automatically apply the first-purchase promotion to a cart, but any customer can use the promotion code at the moment.

So, you need to add custom validation to ensure that the first-purchase promotion is only used by first-time customers.

In this step, you'll customize Medusa's existing workflows to validate the first-purchase promotion usage. You can do that by consuming the [workflows' hooks](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md). A workflow hook is a point in a workflow where you can inject custom functionality as a step function.

You'll consume the hooks of the following workflows:

- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartPromotionsWorkflow/index.html.md): This workflow is used to add or remove promotions from a cart. You'll check that the customer is a first-time customer before allowing the promotion to be added.
- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md): This workflow is used to complete a cart and place an order. You'll validate that the first-purchase promotion is only used by first-time customers before allowing the order to be placed.

### a. Consume `updateCartPromotionsWorkflow.validate` Hook

You'll start by consuming the `validate` hook of the `updateCartPromotionsWorkflow`. This hook is called before any operations are performed in the workflow.

To consume the hook, create the file `src/workflows/hooks/validate-promotion.ts` with the following content:

```ts title="src/workflows/hooks/validate-promotion.ts" highlights={validatePromotionHighlights}
import { 
  updateCartPromotionsWorkflow,
} from "@medusajs/medusa/core-flows"
import { FIRST_PURCHASE_PROMOTION_CODE } from "../../constants"
import { MedusaError } from "@medusajs/framework/utils"

updateCartPromotionsWorkflow.hooks.validate(
  (async ({ input, cart }, { container }) => {
    const hasFirstPurchasePromo = input.promo_codes?.some(
      (code) => code === FIRST_PURCHASE_PROMOTION_CODE
    )

    if (!hasFirstPurchasePromo) {
      return
    }

    if (!cart.customer_id) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "First purchase discount can only be applied to carts with a customer"
      )
    }
    const query = container.resolve("query")

    const { data: [customer] } = await query.graph({
      entity: "customer",
      fields: ["orders.*", "has_account"],
      filters: {
        id: cart.customer_id,
      },
    })

    if (!customer.has_account || (customer?.orders?.length || 0) > 0) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "First purchase discount can only be applied to carts with no previous orders"
      )
    }
  })
)
```

You consume a workflow's hook by calling the `hooks` property of the workflow, then calling the hook you want to consume. In this case, you call the `validate` hook of the `updateCartPromotionsWorkflow`.

The `validate` hook receives a step function as a parameter. The function receives two parameters:

- The hook's input, which differs based on the workflow. In this case, it receives the following properties:
  - `input`: The input of the `updateCartPromotionsWorkflow`, which includes the `promo_codes` to add or remove from the cart.
  - `cart`: The cart being updated.
- The hook or step context object. Most notably, it has a `container` property, which is the Medusa container.

In the step function, you check if the `FIRST_PURCHASE_PROMOTION_CODE` is being applied to the cart. If so, you validate that:

- The cart is associated with a customer.
- The customer has an account.
- The customer has no previous orders.

If any of these validations fail, you throw a `MedusaError` with the appropriate error message. This will prevent the promotion from being applied to the cart.

To retrieve the customer's details, you use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). Query allows you to retrieve data across modules in your Medusa application.

### b. Consume `completeCartWorkflow.validate` Hook

Next, you'll consume the `validate` hook of the `completeCartWorkflow`. This workflow is used to complete a cart and place an order. You'll validate that the first-purchase promotion is only used by first-time customers before allowing the order to be placed.

In the same `src/workflows/hooks/validate-promotion.ts` file, add the following import at the top of the file:

```ts title="src/workflows/hooks/validate-promotion.ts"
import { 
  completeCartWorkflow,
} from "@medusajs/medusa/core-flows"
```

Then, consume the hook at the end of the file:

```ts title="src/workflows/hooks/validate-promotion.ts" highlights={validateCartCompletionHighlights}
completeCartWorkflow.hooks.validate(
  (async ({ input, cart }, { container }) => {
    const hasFirstPurchasePromo = cart.promotions?.some(
      (promo) => promo?.code === FIRST_PURCHASE_PROMOTION_CODE
    )

    if (!hasFirstPurchasePromo) {
      return
    }

    if (!cart.customer_id) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "First purchase discount can only be applied to carts with a customer"
      )
    }

    const query = container.resolve("query")

    const { data: [customer] } = await query.graph({
      entity: "customer",
      fields: ["orders.*", "has_account"],
      filters: {
        id: cart.customer_id,
      },
    })

    if (!customer.has_account || (customer?.orders?.length || 0) > 0) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "First purchase discount can only be applied to carts with no previous orders"
      )
    }
  })
)
```

You consume the `validate` hook of the `completeCartWorkflow` in the same way as the previous hook. The step function receives the cart being completed as an input.

In the step function, you check if the `FIRST_PURCHASE_PROMOTION_CODE` is applied to the cart. If so, you validate that:

- The cart is associated with a customer.
- The customer has an account.
- The customer has no previous orders.

If any of these validations fail, you throw a `MedusaError` with the appropriate error message. This will prevent the order from being placed if the first-purchase promotion is used by a customer who is not a first-time customer.

### Test it Out

To test the custom validation, start the Medusa application and the Next.js Starter Storefront as you did in the previous steps.

Then, register a new customer in the storefront, and place an order. The first-purchase promotion will be applied to the cart automatically and the order will be placed successfully.

Try to place another order with the same customer. The first-purchase promotion will not be automatically applied to the cart. If you also try to apply the first-purchase promotion manually, you'll receive an error message indicating that the promotion can only be applied to first-time customers.

***

## Step 5: Show Discount Pop-Up in Storefront

The first-time purchase promotion is now fully functional. However, you need to inform first-time customers about the discount and encourage them to sign up.

To do that, you'll customize the Next.js Starter Storefront to show a pop-up when a first-time customer visits the storefront.

### a. Create the Pop-Up Component

You'll first create the pop-up component that will be displayed to first-time customers.

Create the file `src/modules/common/components/discount-popup/index.tsx` with the following content:

```tsx title="src/modules/common/components/discount-popup/index.tsx" badgeLabel="Storefront" badgeColor="blue"
"use client"

import { Button, Heading, Text } from "@medusajs/ui"
import Modal from "@modules/common/components/modal"
import useToggleState from "@lib/hooks/use-toggle-state"
import { useEffect } from "react"
import LocalizedClientLink from "@modules/common/components/localized-client-link"

const DISCOUNT_POPUP_KEY = "discount_popup_shown"

const DiscountPopup = () => {
  const { state, open, close } = useToggleState(false)

  useEffect(() => {
    // Check if the popup has been shown before
    const hasBeenShown = localStorage.getItem(DISCOUNT_POPUP_KEY)
    
    if (!hasBeenShown) {
      open()
      // Mark as shown
      localStorage.setItem(DISCOUNT_POPUP_KEY, "true")
    }
  }, [open])

  return (
    <Modal isOpen={state} close={close} size="small" data-testid="discount-popup">
      <div className="relative overflow-hidden bg-gradient-to-br from-amber-50 to-amber-100 rounded-t-lg px-6 pt-8 pb-6">
        {/* Decorative elements */}
        <div className="absolute top-0 right-0 w-20 h-20 bg-amber-200 rounded-full -mr-10 -mt-10 opacity-50"></div>
        <div className="absolute bottom-0 left-0 w-16 h-16 bg-amber-200 rounded-full -ml-8 -mb-8 opacity-40"></div>
        
        <div className="relative">
          {/* Sale tag */}
          <div className="absolute -top-2 -right-2 bg-rose-500 text-white text-xs font-bold px-3 py-1 rounded-full transform rotate-12 shadow-md">
            SAVE 10%
          </div>

          <Heading level="h2" className="text-2xl font-bold text-center text-amber-900">
            Limited Time Offer!
          </Heading>
          
          <div className="flex justify-center my-4">
            <div className="relative">
              <div className="text-5xl font-bold text-rose-600">10%</div>
              <div className="text-lg font-semibold text-amber-900 mt-1">OFF YOUR FIRST ORDER</div>
              <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-8xl text-amber-200 opacity-20 font-bold -z-10">
                %
              </div>
            </div>
          </div>
        </div>
      </div>

      <Modal.Body>
        <div className="flex flex-col items-center gap-y-6 py-6 px-6 bg-white">
          <Text className="text-center text-gray-700">
            Sign up now to receive an exclusive 10% discount on your first purchase. Join our community of satisfied customers!
          </Text>
          
          <div className="flex flex-col gap-y-4 w-full">
            <LocalizedClientLink href="/account" className="w-full">
              <Button 
                variant="primary" 
                className="w-full h-12 font-semibold text-base shadow-md hover:shadow-lg transition-all"
                onClick={close}
              >
                Register & Save 10%
              </Button>
            </LocalizedClientLink>
            
            <Button 
              variant="secondary" 
              className="w-full h-10 font-medium"
              onClick={close}
            >
              Maybe Later
            </Button>
          </div>

          <div className="text-xs text-gray-400 text-center mt-2">
            *Discount applies to your first order only
          </div>
        </div>
      </Modal.Body>
    </Modal>
  )
}

export default DiscountPopup
```

This component uses the `Modal` component that is already available in the Next.js Starter Storefront. It displays a pop-up with a discount offer and two buttons: one to register and save the discount, and another to close the pop-up.

The pop-up will only be shown to first-time customers. Once the pop-up is shown, a `discount_popup_shown` key is stored in the local storage to prevent it from being shown again.

### b. Add the Pop-Up to Layout

To ensure that the pop-up is displayed when the customer visits the storefront, you need to add the `DiscountPopup` component to the main layout of the Next.js Starter Storefront.

In `src/app/[countryCode]/(main)/layout.tsx`, add the following import at the top of the file:

```tsx title="src/app/[countryCode]/(main)/layout.tsx" badgeLabel="Storefront" badgeColor="blue"
import DiscountPopup from "@modules/common/components/discount-popup"
```

Then, in the return statement of the `PageLayout` component, add the `DiscountPopup` component before rendering `props.children`:

```tsx title="src/app/[countryCode]/(main)/layout.tsx" badgeLabel="Storefront" badgeColor="blue"
<>
  {/* ... */}
  {!customer && <DiscountPopup />}
  {props.children}
  {/* ... */}
</>
```

Notice that you only display the pop-up if the customer is not logged in. This way, the pop-up will only be shown to first-time customers.

### c. Show Registration Form Before Login

If you go to the `/account` page in the Next.js Starter Storefront as a guest customer, you'll see the login form. However, in this case, you want to show the registration form first instead.

To change this behavior, in `src/modules/account/templates/login-template.tsx`, change the default value of `currentView` to `"register"`:

```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue"
const [currentView, setCurrentView] = useState("register")
```

This way, when a guest customer visits the `/account` page, they will see the registration form instead of the login form.

### Test it Out

To test the pop-up, start the Medusa application and the Next.js Starter Storefront as you did in the previous steps.

Then, open the storefront in your browser. If you're a first-time customer, you'll see the discount pop-up encouraging you to sign up and receive the first-purchase discount.

If you don't see the pop-up, make sure that you're logged out.

![Discount pop-up in the Next.js Starter Storefront](https://res.cloudinary.com/dza7lstvk/image/upload/v1750844087/Medusa%20Resources/CleanShot_2025-06-25_at_12.34.35_2x_f1f5jh.png)

***

## Next Steps

You've now implemented the first-purchase discount feature in Medusa. You can add more features to build customer loyalty, such as a [loyalty points system](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/how-to-tutorials/tutorials/loyalty-points/index.html.md) or [product reviews](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/how-to-tutorials/tutorials/product-reviews/index.html.md).

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.


# Add Gift Message to Line Items in Medusa

In this tutorial, you will learn how to add a gift message to items in carts and orders in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md), which are available out-of-the-box. These features include cart and order management capabilities.

You can customize the Medusa application and storefront to add a gift message to items in the cart. This feature allows customers to add a personalized message to their gifts, enhancing the shopping experience.

## Summary

By following this tutorial, you will learn how to:

- Install and set up Medusa and the Next.js Starter Storefront.
- Customize the storefront to support gift messages on cart items during checkout.
- Customize the Medusa Admin to show gift items with messages in an order.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

[View on Github](https://github.com/medusajs/examples/tree/main/order-gift-message): Find the full code for this tutorial.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

First, you'll be asked for the project's name. Then, when prompted about installing the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."

Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Add Gift Inputs to Cart Item

In this step, you'll customize the Next.js Starter Storefront to allow customers to specify that an item is a gift and add a gift message to it.

You'll store the gift option and message in the cart item's `metadata` property, which is a key-value `jsonb` object that can hold any additional information about the item. When the customer places the order, the `metadata` is copied to the `metadata` of the order's line items.

So, you only need to customize the storefront to add the gift message input and update the cart item metadata.

### a. Changes to Update Item Function

The Next.js Starter Storefront has an `updateLineItem` function that sends a request to the Medusa server to update the cart item. However, it doesn't support updating the `metadata` property.

So, in `src/lib/data/cart.ts`, find the `updateLineItem` function and add a `metadata` property to its object parameter:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"], ["8"]]}
export async function updateLineItem({
  lineId,
  quantity,
  metadata,
}: {
  lineId: string
  quantity: number
  metadata?: Record<string, any>
}) {
  // ...
}
```

Next, change the usage of `await sdk.store.cart.updateLineItem` in the function to pass the `metadata` property:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
const updateData: any = { quantity }
if (metadata) {
  updateData.metadata = metadata
}

await sdk.store.cart
  .updateLineItem(cartId, lineId, updateData, {}, headers)
// ...
```

You pass the `metadata` property to the Medusa server, which will update the cart item with the new metadata.

### b. Add Gift Inputs

Next, you'll modify the cart item component that's shown in the cart and checkout pages to show two inputs: one to specify that the item is a gift and another to add a gift message.

In `src/modules/cart/components/item/index.tsx`, add the following imports at the top of the file:

```tsx title="src/modules/cart/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { Checkbox, Textarea, Button, Label } from "@medusajs/ui"
```

You import components from the [Medusa UI library](https://docs.medusajs.com/ui/index.html.md) that will be useful for the gift inputs.

Next, in the `Item` component, add the following variables before the `changeQuantity` function:

```tsx title="src/modules/cart/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={giftVarsHighlights}
const [giftUpdating, setGiftUpdating] = useState(false)
const [newGiftMessage, setNewGiftMessage] = useState(
  item.metadata?.gift_message as string || ""
)
const [isEditingGiftMessage, setIsEditingGiftMessage] = useState(false)

const isGift = item.metadata?.is_gift === "true"
const giftMessage = item.metadata?.gift_message as string
```

You define the following variables:

- `giftUpdating`: A state variable to track whether the gift message is being updated. This will be useful to handle loading and disabled states.
- `newGiftMessage`: A state variable to hold the new gift message input value.
- `isEditingGiftMessage`: A state variable to track whether the gift message input is being edited. This will be useful to show or hide the input field.
- `isGift`: A boolean indicating whether the item is a gift based on the `metadata.is_gift` property.
- `giftMessage`: The current gift message from the item's `metadata.gift_message` property.

Next, add the following functions before the `return` statement to handle updates to the gift inputs:

```tsx title="src/modules/cart/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={giftFunctionsHighlights}
const handleGiftToggle = async (checked: boolean) => {
  setGiftUpdating(true)
  
  try {
    const newMetadata = {
      is_gift: checked.toString(),
      gift_message: checked ? newGiftMessage : "",
    }
    
    await updateLineItem({
      lineId: item.id,
      quantity: item.quantity,
      metadata: newMetadata,
    })
  } catch (error) {
    console.error("Error updating gift status:", error)
  } finally {
    setGiftUpdating(false)
  }
}

const handleSaveGiftMessage = async () => {
  setGiftUpdating(true)
  
  try {
    const newMetadata = {
      is_gift: "true",
      gift_message: newGiftMessage,
    }
    
    await updateLineItem({
      lineId: item.id,
      quantity: item.quantity,
      metadata: newMetadata,
    })
    setIsEditingGiftMessage(false)
  } catch (error) {
    console.error("Error updating gift message:", error)
  } finally {
    setGiftUpdating(false)
  }
}

const handleStartEdit = () => {
  setIsEditingGiftMessage(true)
}

const handleCancelEdit = () => {
  setNewGiftMessage(giftMessage || "")
  setIsEditingGiftMessage(false)
}
```

You define the following functions:

- `handleGiftToggle`: Used when the gift checkbox is toggled. It updates the cart item's metadata to set the `is_gift` and `gift_message` properties based on the checkbox state.
- `handleSaveGiftMessage`: Used to save the gift message when the customer clicks the "Save" button. It updates the cart item's metadata with the new gift message.
- `handleStartEdit`: Used to start editing the gift message input by setting the `isEditingGiftMessage` state to `true`.
- `handleCancelEdit`: Used to cancel the gift message editing and reset the input value to the current gift message.

Finally, you'll change the `return` statement to include the gift inputs. Replace the existing return statement with the following:

```tsx title="src/modules/cart/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div className="bg-white border border-gray-200 rounded-lg p-4">
    <div className="flex gap-4">
      {/* Product Image */}
      <div className="flex-shrink-0">
        <LocalizedClientLink
          href={`/products/${item.product_handle}`}
          className={clx("flex", {
            "w-16": type === "preview",
            "w-20": type === "full",
          })}
        >
          <Thumbnail
            thumbnail={item.thumbnail}
            images={item.variant?.product?.images}
            size="square"
          />
        </LocalizedClientLink>
      </div>

      {/* Product Details */}
      <div className="flex-1 min-w-0">
        <div className="flex justify-between items-start mb-2">
          <div className="flex-1 min-w-0">
            <Text
              className="txt-medium-plus text-ui-fg-base truncate"
              data-testid="product-title"
            >
              {item.product_title}
            </Text>
            <LineItemOptions variant={item.variant} data-testid="product-variant" />
          </div>
        </div>

        {/* Gift Options */}
        <div className="mb-3">
          <div
            className="inline-flex items-center gap-2 px-3 py-1.5 bg-gray-50 rounded-full border hover:bg-gray-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
          >
            <Checkbox
              checked={isGift}
              onCheckedChange={handleGiftToggle}
              disabled={giftUpdating}
              className="w-4 h-4"
              id="is-gift"
            />
            <Label htmlFor="is-gift">
              This is a gift
            </Label>
          </div>
          
          {isGift && (
            <div className="mt-3">
              {isEditingGiftMessage ? (
                <div className="space-y-2">
                  <div className="flex items-center gap-2">
                    <Text className="text-sm font-medium text-ui-fg-base">
                      Gift Message:
                    </Text>
                    <Text className="text-xs text-ui-fg-subtle">(optional)</Text>
                  </div>
                  <Textarea
                    placeholder="Add a personal message..."
                    value={newGiftMessage}
                    onChange={(e) => setNewGiftMessage(e.target.value)}
                    disabled={giftUpdating}
                    className="w-full"
                    rows={2}
                  />
                  <div className="flex justify-end gap-2">
                    <Button
                      size="small"
                      variant="secondary"
                      onClick={handleCancelEdit}
                      disabled={giftUpdating}
                      className="text-xs px-3 py-1"
                    >
                      Cancel
                    </Button>
                    <Button
                      size="small"
                      variant="primary"
                      onClick={handleSaveGiftMessage}
                      disabled={giftUpdating || newGiftMessage === giftMessage}
                      className="text-xs px-3 py-1"
                    >
                      {giftUpdating ? <Spinner /> : "Save"}
                    </Button>
                  </div>
                </div>
              ) : (
                <div className="flex items-center justify-between">
                  <div className="flex items-center gap-2">
                    <Text className="text-sm font-medium text-ui-fg-base">
                      Gift Message:
                    </Text>
                    {giftMessage ? (
                      <Text className="text-sm text-ui-fg-subtle">
                        {giftMessage}
                      </Text>
                    ) : (
                      <Text className="text-sm text-ui-fg-subtle italic">
                        No message added
                      </Text>
                    )}
                  </div>
                  <Button
                    size="small"
                    variant="secondary"
                    onClick={handleStartEdit}
                    className="text-xs px-2 py-1"
                  >
                    {giftMessage ? "Edit" : "Add"}
                  </Button>
                </div>
              )}
            </div>
          )}
        </div>

        {/* Quantity and Actions */}
        {type === "full" && (
          <div className="flex items-center justify-between">
            <div className="flex items-center gap-2">
              <DeleteButton id={item.id} data-testid="product-delete-button" />
              <CartItemSelect
                value={item.quantity}
                onChange={(value) => changeQuantity(parseInt(value.target.value))}
                className="w-16 h-8 p-2"
                data-testid="product-select-button"
              >
                {/* TODO: Update this with the v2 way of managing inventory */}
                {Array.from(
                  {
                    length: Math.min(maxQuantity, 10),
                  },
                  (_, i) => (
                    <option value={i + 1} key={i}>
                      {i + 1}
                    </option>
                  )
                )}
              </CartItemSelect>
              {updating && <Spinner />}
            </div>
          </div>
        )}

        {/* Preview Mode */}
        {type === "preview" && (
          <div className="flex items-center justify-between">
            <Text className="text-sm text-ui-fg-subtle">
              Qty: {item.quantity}
            </Text>
            <LineItemUnitPrice
              item={item}
              style="tight"
              currencyCode={currencyCode}
            />
          </div>
        )}

        <ErrorMessage error={error} data-testid="product-error-message" />
      </div>
    </div>
  </div>
)
```

You replace the previous table row design with a card. In the card, you show the item's image, title, variant options, quantity, and price.

You also show a checkbox to toggle the gift status of the item. If the item is a gift, you show a text area to add or edit the gift message. You only show the gift message input if the user clicks an "Add" or "Edit" button.

The gift message input has a "Save" button to save the message and a "Cancel" button to cancel the editing.

### c. Update Cart and Checkout Templates

The items are previously shown on the cart and checkout pages in a table. However, since you've changed the item component to a card, you'll need to update the cart and checkout templates to replace the table with a list of items.

In `src/modules/cart/templates/items.tsx`, which shows the items on the cart page, change the `return` statement to the following:

```tsx title="src/modules/cart/templates/items.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div>
    <div className="pb-3 flex items-center">
      <Heading className="text-[2rem] leading-[2.75rem]">Cart</Heading>
    </div>
    <div className="space-y-4">
      {items
        ? items
            .sort((a, b) => {
              return (a.created_at ?? "") > (b.created_at ?? "") ? -1 : 1
            })
            .map((item) => {
              return (
                <Item
                  key={item.id}
                  item={item}
                  currencyCode={cart?.currency_code}
                />
              )
            })
        : repeat(5).map((i) => {
            return <SkeletonLineItem key={i} />
          })}
    </div>
  </div>
)
```

You replace the table that was wrapping the items with a `div` that contains a list of `Item` components.

Next, in `src/modules/cart/templates/preview.tsx` that shows the items in the checkout summary, change the `return` statement to the following:

```tsx title="src/modules/cart/templates/preview.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div
    className={clx("space-y-3 mt-4", {
      "pl-[1px] overflow-y-scroll overflow-x-hidden no-scrollbar max-h-[420px]":
        hasOverflow,
    })}
  >
    {items
      ? items
          .sort((a, b) => {
            return (a.created_at ?? "") > (b.created_at ?? "") ? -1 : 1
          })
          .map((item) => {
            return (
              <Item
                key={item.id}
                item={item}
                type="preview"
                currencyCode={cart.currency_code}
              />
            )
          })
      : repeat(5).map((i) => {
          return <SkeletonLineItem key={i} />
        })}
  </div>
)
```

Similarly, you replace the table that was wrapping the items with a `div` that contains a list of `Item` components.

### Test the Changes

You can now test the gift inputs during checkout.

First, start the Medusa application by running the following command in the Medusa application directory:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

Then, start the Next.js Starter Storefront by running the following command in the storefront directory:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

Open the storefront in your browser at `http://localhost:8000`. Add a product to the cart, and proceed to the checkout page. You'll find a checkbox to mark the item as a gift for each item.

![Gift checkbox on checkout page](https://res.cloudinary.com/dza7lstvk/image/upload/v1750923117/Medusa%20Resources/CleanShot_2025-06-26_at_10.31.25_2x_wcqiff.png)

If you check the box, you can also add and edit the gift message. The gift message will be saved in the cart item's `metadata`.

![Gift item with message added](https://res.cloudinary.com/dza7lstvk/image/upload/v1750924150/Medusa%20Resources/CleanShot_2025-06-26_at_10.32.52_2x_qir2t5.png)

***

## Step 3: Show Gift Options in Order Confirmation Page

Next, you'll customize the storefront to show the gift message of items in the order confirmation page.

In `src/modules/order/components/item/index.tsx`, add in the `Item` component the following variables:

```tsx title="src/modules/order/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const isGift = item.metadata?.is_gift === "true"
const giftMessage = item.metadata?.gift_message as string
```

You define the following variables:

- `isGift`: A boolean indicating whether the item is a gift based on the `metadata.is_gift` property.
- `giftMessage`: The item's gift message from the `metadata.gift_message` property.

Next, in the `return` statement, add the following below the `LineItemOptions` component:

```tsx title="src/modules/order/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue"
{isGift && <Text
  className="inline-block txt-medium text-ui-fg-subtle w-full overflow-hidden text-ellipsis"
>
  Gift Message: {giftMessage || "No gift message provided"}
</Text>}
```

If the item is a gift, you show the gift message below the variant options. If no gift message is provided, you show "No gift message provided."

### Test the Changes

To test this change, place an order with a gift item. On the order confirmation page, you should see the gift message displayed below the variant options.

![Gift message on order confirmation page](https://res.cloudinary.com/dza7lstvk/image/upload/v1750924429/Medusa%20Resources/CleanShot_2025-06-26_at_10.53.32_2x_htzvtw.png)

***

## Step 4: Show Gift Options in Admin Dashboard

In this step, you'll customize the Medusa Admin dashboard to show the gift items with their messages in an order.

### What is a Widget?

The Medusa Admin dashboard's pages are customizable to insert [widgets](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md) of custom content in predefined injection zones. You create these widgets as React components that allow admin users to perform custom actions.

So, to show the gift items with their messages in an order, you'll create a custom widget that shows the gift items in the order details page.

### Create the Widget

You create a widget in a `.tsx` file under the `src/admin/widgets` directory. So, in the Medusa application directory, create the file `src/admin/widgets/order-gift-items-widget.tsx` with the following content:

```tsx title="src/admin/widgets/order-gift-items-widget.tsx" badgeLabel="Medusa Application" badgeColor="green"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Text } from "@medusajs/ui"
import { DetailWidgetProps, AdminOrder } from "@medusajs/framework/types"

const OrderGiftItemsWidget = ({ data }: DetailWidgetProps<AdminOrder>) => {
  const giftItems = data.items?.filter(
    (item: any) => item.metadata?.is_gift === "true"
  )

  if (!giftItems?.length) {
    return null
  }

  return (
    <Container className="mb-4">
      <Text className="font-medium h2-core mb-2">Gift Items & Messages</Text>
      <div className="flex flex-col gap-4">
        {giftItems.map((item: any) => (
          <div key={item.id} className="border-b last:border-b-0 pb-2">
            <Text className="font-medium">{item.title} (x{item.quantity})</Text>
            <Text className="text-sm text-gray-600">
              Gift Message: {item.metadata?.gift_message || "(No message)"}
            </Text>
          </div>
        ))}
      </div>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "order.details.side.after",
})

export default OrderGiftItemsWidget 
```

A widget file must export:

- A React component that renders the widget content.
- A configuration object created with `defineWidgetConfig` that defines the widget's [injection zone](https://docs.medusajs.com/admin-widget-injection-zones/index.html.md).

You define the `OrderGiftItemsWidget` component that is injected in the `order.details.side.after` zone. Because it's injected in the order details page, it receives the order details as a `data` prop.

In the component, you filter the order items to get only the gift items by checking if the `metadata.is_gift` property is set to `"true"`. Then, you render the gift items with their messages in a new "Gift Items & Messages" section.

### Test Medusa Admin Changes

To test out the Medusa Admin widget, start the Medusa application by running the following command in the Medusa application directory:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

Then, open the Medusa Admin at `http://localhost:9000/app` and log in with the user you created earlier.

Go to the Orders page and click on an order that has gift items. You'll find a new section called "Gift Items & Messages" that shows the gift items with their messages.

![Order details page in the Medusa Admin with the "Gift Items & Messages" section highlighted](https://res.cloudinary.com/dza7lstvk/image/upload/v1750925501/Medusa%20Resources/CleanShot_2025-06-26_at_11.11.18_2x_jrrc2s.png)

***

## Optional: Handle Gift Items in Fulfillment Provider

If you have a custom fulfillment provider and you want to handle gift items in it, you can do so in the `createFulfillment` method of the [Fulfillment Module Provider's service](https://docs.medusajs.com/references/fulfillment/provider/index.html.md).

For example:

```ts title="Fulfillment Module Provider's Service" badgeLabel="Medusa Application" badgeColor="green" highlights={createFulfillmentHighlights}
class ManualFulfillmentService extends utils_1.AbstractFulfillmentProviderService {
  // ...
  async createFulfillment(
    data: Record<string, unknown>,
    items: Partial<Omit<FulfillmentItemDTO, "fulfillment">>[],
    order: Partial<FulfillmentOrderDTO> | undefined,
    fulfillment: Partial<Omit<FulfillmentDTO, "provider_id" | "data" | "items">>
  ): Promise<CreateFulfillmentResult> {
    const itemsWithGiftMessage = order.items?.filter((lineItem) => {
      const isInFulfillment = items.find(
        (item) => item.line_item_id === lineItem.id
      )
      if (!isInFulfillment) {
        return false
      }
      return lineItem.metadata?.is_gift === "true"
    })

    // TODO pass gift items to third-party provider
  }
}
```

You filter the order items to find the items that are part of the fulfillment and are gift items. You can then process or pass them differently to your third-party provider.

***

## Next Steps

You've now added gift messages to items in carts and orders in Medusa.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.


# Generate Invoices for Orders in Medusa

In this tutorial, you will learn how to generate invoices for orders in your Medusa application.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md), which are available out-of-the-box. These features include order management capabilities.

You can extend the Medusa application to automatically generate invoices (or sales orders), manage invoice configurations through the admin dashboard, and provide customers with easy access to their invoices through the storefront.

## Summary

By following this tutorial, you will learn how to:

- Install and set up Medusa.
- Store default invoice configurations and manage them from the Medusa Admin dashboard.
- Generate PDF invoices for orders, and allow admin users and customers to download them.
- Send PDF invoices as attachments in order confirmation emails.
- Mark previously generated invoices as stale when orders are updated.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Diagram illustrating the flow from the customer placing the order, them receiving the invoice, and the admin downloading the invoice](https://res.cloudinary.com/dza7lstvk/image/upload/v1753793501/Medusa%20Resources/invoices-summary_ieja9e.jpg)

- [Full Code](https://github.com/medusajs/examples/tree/main/invoice-generator): Find the full code for this tutorial.
- [OpenAPI Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1753801608/OpenApi/Invoice-Generator_wtft9v.yaml): Import this OpenAPI Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.

Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Invoice Generator Module

In Medusa, you can build custom features in a [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a reusable package with the data models and functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

In this step, you'll build an Invoice Generator Module that defines the data models and logic to manage invoices. Later, you'll build commerce flows related to invoices around the module.

Refer to the [Modules documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to learn more.

### a. Create Module Directory

Create the directory `src/modules/invoice-generator` that will hold the Invoice Generator Module's code.

### b. Create Data Models

A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

Refer to the [Data Models documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md) to learn more.

For the Invoice Generator Module, you'll create a data model to store default invoice configurations and another to store generated invoices.

#### InvoiceConfig Data Model

To create the first data model, create the file `src/modules/invoice-generator/models/invoice-config.ts` with the following content:

```ts title="src/modules/invoice-generator/models/invoice-config.ts" highlights={invoiceConfigHighlights}
import { model } from "@medusajs/framework/utils"

export const InvoiceConfig = model.define("invoice_config", {
  id: model.id().primaryKey(),
  company_name: model.text(),
  company_address: model.text(),
  company_phone: model.text(),
  company_email: model.text(),
  company_logo: model.text().nullable(),
  notes: model.text().nullable(),
})
```

The `InvoiceConfig` data model has the following properties:

- `id`: The primary key of the table.
- `company_name`: The name of the company issuing the invoice.
- `company_address`: The address of the company.
- `company_phone`: The phone number of the company.
- `company_email`: The email address of the company.
- `company_logo`: The URL of the company logo image.
- `notes`: Additional notes to include in the invoice.

You can also add other fields as needed, such as tax information or payment terms.

Learn more about defining data model properties in the [Property Types documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md).

#### Invoice Data Model

Next, you'll create the `Invoice` data model that represents a generated invoice.

Create the file `src/modules/invoice-generator/models/invoice.ts` with the following content:

```ts title="src/modules/invoice-generator/models/invoice.ts" highlights={invoiceHighlights}
import { model } from "@medusajs/framework/utils"

export enum InvoiceStatus {
  LATEST = "latest",
  STALE = "stale",
}

export const Invoice = model.define("invoice", {
  id: model.id().primaryKey(),
  display_id: model.autoincrement(),
  order_id: model.text(),
  status: model.enum(InvoiceStatus).default(InvoiceStatus.LATEST),
  pdfContent: model.json(),
})
```

The `Invoice` data model has the following properties:

- `id`: The primary key of the table.
- `display_id`: An auto-incrementing identifier for the invoice, which will be used to display the invoice number.
- `order_id`: The ID of the order that the invoice belongs to.
- `status`: The current status of the invoice.
  - `latest` indicates the invoice is the most recent version for the order.
  - `stale` indicates a previous invoice for the order that became stale after order updates.
- `pdfContent`: The content of the invoice in object format that works with `pdfmake`. You'll use this later to generate the invoice PDF.

### c. Create Module's Service

You can manage your module's data models in a service.

A service is a TypeScript class that the module exports. In the service's methods, you can connect to the database, allowing you to manage your data models, or connect to a third-party service, which is useful if you're integrating with external services.

Refer to the [Module Service documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md) to learn more.

To create the Invoice Generator Module's service, create the file `src/modules/invoice-generator/service.ts` with the following content:

```ts title="src/modules/invoice-generator/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import { InvoiceConfig } from "./models/invoice-config"
import { Invoice } from "./models/invoice"

class InvoiceGeneratorService extends MedusaService({
  InvoiceConfig,
  Invoice,
}) { }

export default InvoiceGeneratorService
```

The `InvoiceGeneratorService` extends `MedusaService`, which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods.

So, the `InvoiceGeneratorService` class now has methods like `createInvoices` and `retrieveInvoice`.

Find all methods generated by the `MedusaService` in [the Service Factory reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md).

### d. Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/invoice-generator/index.ts` with the following content:

```ts title="src/modules/invoice-generator/index.ts"
import InvoiceModuleService from "./service"
import { Module } from "@medusajs/framework/utils"

export const INVOICE_MODULE = "invoiceGenerator"

export default Module(INVOICE_MODULE, {
  service: InvoiceModuleService,
})
```

You use the `Module` function to create the module's definition. It accepts two parameters:

1. The module's unique name.
2. An object whose `service` property is the module's service class.

### e. Register the Module

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/invoice-generator",
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

### f. Generate and Run Migrations

Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript class that defines database changes made by a module.

Refer to the [Migrations documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md) to learn more.

Medusa's CLI tool can generate the migrations for you. To generate a migration for the Invoice Generator Module, run the following command in your Medusa application's directory:

```bash
npx medusa db:generate invoice-generator
```

The `db:generate` command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a `migrations` directory under `src/modules/invoice-generator` that holds the generated migration.

Then, to reflect these migrations on the database, run the following command:

```bash
npx medusa db:migrate
```

The tables for the `InvoiceConfig` and `Invoice` data models are now created in the database.

***

## Step 3: Create Default Invoice Configurations

In this step, you'll create default invoice configurations that admins can later manage through the Medusa Admin dashboard.

Since you need to create the default configurations when the Medusa application starts, you'll use a [loader](https://docs.medusajs.com/docs/learn/fundamentals/modules/loaders/index.html.md). A loader is a script in your module that Medusa runs on application startup.

### a. Create Loader

To create the loader, create the file `src/modules/invoice-generator/loaders/create-default-config.ts` with the following content:

```ts title="src/modules/invoice-generator/loaders/create-default-config.ts" highlights={createDefaultConfigHighlights}
import {
  LoaderOptions,
  IMedusaInternalService,
} from "@medusajs/framework/types"
import { InvoiceConfig } from "../models/invoice-config"

export default async function createDefaultConfigLoader({
  container,
}: LoaderOptions) {
  const service: IMedusaInternalService<
    typeof InvoiceConfig
  > = container.resolve("invoiceConfigService")

  const [_, count] = await service.listAndCount()

  if (count > 0) {
    return
  }

  await service.create({
    company_name: "Acme",
    company_address: "123 Acme St, Springfield, USA",
    company_phone: "+1 234 567 8900",
    company_email: "admin@example.com",
  })
}
```

The loader function accepts an object having the [module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) as a parameter. You can use this container to resolve Framework tools and the module's resources.

In the loader, you resolve a service that Medusa generates for the `InvoiceConfig` data model. You use that service to create default configurations if none exist.

### b. Register the Loader

Next, to register the loader in the module's definition, update the `Module` function usage in `src/modules/invoice-generator/index.ts`:

```ts title="src/modules/invoice-generator/index.ts" highlights={loaderRegisterHighlights}
// other imports...
import createDefaultConfigLoader from "./loaders/create-default-config"

// ...

export default Module(INVOICE_MODULE, {
  // ...
  loaders: [createDefaultConfigLoader],
})
```

You pass a `loaders` property to the `Module` function, whose value is an array of loader functions.

Refer to the [Loaders](https://docs.medusajs.com/docs/learn/fundamentals/modules/loaders/index.html.md) documentation to learn more about loaders.

### c. Run the Loader

To run the loader, start the Medusa application:

```bash npm2yarn
npm run dev
```

The Medusa application will run your loader to create the default invoice configurations in the database. You'll verify that it worked in the next step after adding the admin settings page.

***

## Step 4: Allow Admins to Manage Invoice Configurations

In this step, you'll customize the Medusa application to allow admin users to manage the default invoice configurations through the Medusa Admin dashboard.

To build this feature, you need to create:

1. A [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) with the business logic to manage invoice configurations.
2. An [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) that exposes the invoice configuration management functionality to clients.
3. A [settings page](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes#create-settings-page/index.html.md) in the Medusa Admin dashboard that allows admin users to manage the invoice configurations.

### a. Invoice Configuration Management Workflow

A workflow is a series of queries and actions, called steps, that complete a task. A workflow is similar to a function, but it allows you to track its executions' progress, define roll-back logic, and configure other advanced features.

Refer to the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) to learn more.

The workflow that manages invoice configurations will have a single step that updates the invoice configuration.

#### Update Invoice Configuration Step

To create a step, create the file `src/workflows/steps/update-invoice-config.ts` with the following content:

If you get a type error on resolving the Invoice Generator Module, run the Medusa application once with the `npm run dev` or `yarn dev` command to generate the necessary type definitions, as explained in the [Automatically Generated Types guide](https://docs.medusajs.com/docs/learn/fundamentals/generated-types/index.html.md).

```ts title="src/workflows/steps/update-invoice-config.ts" highlights={updateInvoiceConfigHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { INVOICE_MODULE } from "../../modules/invoice-generator"

type StepInput = {
  id?: string
  company_name?: string
  company_address?: string
  company_phone?: string
  company_email?: string
  company_logo?: string
  notes?: string
}

export const updateInvoiceConfigStep = createStep(
  "update-invoice-config",
  async ({ id, ...updateData }: StepInput, { container }) => {
    const invoiceGeneratorService = container.resolve(INVOICE_MODULE)

    const prevData = id ? 
      await invoiceGeneratorService.retrieveInvoiceConfig(id) : 
      (await invoiceGeneratorService.listInvoiceConfigs())[0]

    const updatedData = await invoiceGeneratorService.updateInvoiceConfigs({
      id: prevData.id,
      ...updateData,
    })

    return new StepResponse(updatedData, prevData)
  },
  async (prevInvoiceConfig, { container }) => {
    if (!prevInvoiceConfig) {
      return
    }

    const invoiceGeneratorService = container.resolve(INVOICE_MODULE)

    await invoiceGeneratorService.updateInvoiceConfigs({
      id: prevInvoiceConfig.id,
      company_name: prevInvoiceConfig.company_name,
      company_address: prevInvoiceConfig.company_address,
      company_phone: prevInvoiceConfig.company_phone,
      company_email: prevInvoiceConfig.company_email,
      company_logo: prevInvoiceConfig.company_logo,
    })
  }
)
```

You create a step with the `createStep` function. It accepts three parameters:

1. The step's unique name.
2. An async function that receives two parameters:
   - The step's input, which is an object having the data to update in the invoice configuration.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.
3. An async compensation function that undoes the actions performed by the step function. This function is only executed if an error occurs during the workflow's execution.

In the step function, you resolve the Invoice Generator Module's service from the Medusa container. Then, you either retrieve the invoice configuration by its ID, or list all invoice configurations and use the first one.

Next, you update the invoice configuration with the data passed to the step function.

Finally, a step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters:

1. The step's output, which is the updated invoice configuration.
2. Data to pass to the step's compensation function.

In the compensation function, you undo the invoice configuration updates if an error occurs during the workflow's execution.

#### Update Invoice Configuration Workflow

Next, you'll create a workflow that uses the `updateInvoiceConfigStep` step to update the invoice configuration.

Create the file `src/workflows/update-invoice-config.ts` with the following content:

```ts title="src/workflows/update-invoice-config.ts"
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { updateInvoiceConfigStep } from "./steps/update-invoice-config"

type WorkflowInput = {
  id?: string
  company_name?: string
  company_address?: string
  company_phone?: string
  company_email?: string
  company_logo?: string
  notes?: string
}

export const updateInvoiceConfigWorkflow = createWorkflow(
  "update-invoice-config",
  (input: WorkflowInput) => {
    const invoiceConfig = updateInvoiceConfigStep(input)

    return new WorkflowResponse({
      invoice_config: invoiceConfig,
    })
  }
)
```

You create a workflow using the `createWorkflow` function. It accepts the workflow's unique name as a first parameter.

It accepts a second parameter: a constructor function that holds the workflow's implementation. The function accepts an input object with the invoice configuration data to update.

In the workflow, you update the invoice configuration using the `updateInvoiceConfigStep` step.

A workflow must return an instance of `WorkflowResponse` that accepts the data to return to the workflow's executor.

### b. Invoice Configuration API Routes

Next, you'll create two API routes:

1. An API route to retrieve the default invoice configurations. This is useful to display the current configurations on the settings page.
2. An API route to update the default invoice configurations. This is useful to allow admin users to update the configurations from the settings page.

#### Retrieve Invoice Configurations API Route

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

Refer to the [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) to learn more about them.

Create the file `src/api/admin/invoice-config/route.ts` with the following content:

```ts title="src/api/admin/invoice-config/route.ts" highlights={retrieveInvoiceConfigHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const query = req.scope.resolve("query")

  const { data: [invoiceConfig] } = await query.graph({
    entity: "invoice_config",
    fields: ["*"],
  })

  res.json({
    invoice_config: invoiceConfig,
  })
}
```

Since you export a `GET` route handler function, you expose a `GET` API route at `/admin/invoice-config`.

In the route handler, you resolve [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) from the Medusa container. It allows you to retrieve data across modules.

You retrieve the default invoice configuration and return it in the response.

#### Update Invoice Configurations API Route

Next, you'll add the API route to update the invoice configurations.

In the same file, add the following imports at the top of the file:

```ts title="src/api/admin/invoice-config/route.ts"
import { z } from "zod"
import { 
  updateInvoiceConfigWorkflow,
} from "../../../workflows/update-invoice-config"
```

Then, add the following at the end of the file:

```ts title="src/api/admin/invoice-config/route.ts" highlights={postInvoiceConfigHighlights}
export const PostInvoiceConfigSchema = z.object({
  company_name: z.string().optional(),
  company_address: z.string().optional(),
  company_phone: z.string().optional(),
  company_email: z.string().optional(),
  company_logo: z.string().optional(),
  notes: z.string().optional(),
})

type PostInvoiceConfig = z.infer<typeof PostInvoiceConfigSchema>

export async function POST(
  req: MedusaRequest<PostInvoiceConfig>,
  res: MedusaResponse
) {
  const { result: { invoice_config } } = await updateInvoiceConfigWorkflow(
    req.scope
  ).run({
    input: req.validatedBody,
  })

  res.json({
    invoice_config,
  })
}
```

You define a validation schema with [Zod](https://zod.dev/) to validate incoming request bodies. The schema defines the fields that can be updated in the invoice configuration.

Next, since you export a `POST` route handler function, you expose a `POST` API route at `/admin/invoice-config`.

In the route handler, you execute the `updateInvoiceConfigWorkflow` by invoking it, passing it the Medusa container, then executing its `run` method.

You return the updated invoice configuration in the response.

#### Add Validation Middleware

To validate the body parameters of requests sent to the API route, you need to apply a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

To apply a middleware to a route, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import { defineMiddlewares, validateAndTransformBody } from "@medusajs/framework/http"
import { PostInvoiceConfigSchema } from "./admin/invoice-config/route"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/admin/invoice-config",
      methods: ["POST"],
      middlewares: [
        validateAndTransformBody(PostInvoiceConfigSchema),
      ],
    },
  ],
})
```

You apply Medusa's `validateAndTransformBody` middleware to `POST` requests sent to the `/admin/invoice-config` API route.

The middleware function accepts a Zod schema, which you created in the API route's file.

Refer to the [Middlewares](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) documentation to learn more.

### c. Create Settings Page

Next, you'll create a settings page in the Medusa Admin dashboard that allows admin users to manage the invoice configurations.

#### Initialize JS SDK

To send requests to the Medusa server, you'll use the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md). It's already installed in your Medusa project, but you need to initialize it before using it in your customizations.

Create the file `src/admin/lib/sdk.ts` with the following content:

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

Learn more about the initialization options in the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) reference.

#### Create Settings Page Component

Settings pages are [UI routes](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes#create-settings-page/index.html.md) created under the `/src/admin/routes/settings` directory. Medusa will then add the settings page under the Settings section of the Medusa Admin dashboard.

Refer to the [UI Routes](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) documentation to learn more.

Create the file `src/admin/routes/settings/invoice-config/page.tsx` with the following content:

```tsx title="src/admin/routes/settings/invoice-config/page.tsx" highlights={invoiceConfigPageHighlights}
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading, Button, Input, Label, Textarea, toast } from "@medusajs/ui"
import { useMutation, useQuery } from "@tanstack/react-query"
import { sdk } from "../../../lib/sdk"
import { useForm } from "react-hook-form"
import * as zod from "zod"
import { 
  FormProvider,
  Controller,
} from "react-hook-form"
import { useCallback, useEffect } from "react"

type InvoiceConfig = {
  id: string;
  company_name: string;
  company_address: string;
  company_phone: string;
  company_email: string;
  company_logo?: string;
  notes?: string;
}

const schema = zod.object({
  company_name: zod.string().optional(),
  company_address: zod.string().optional(),
  company_phone: zod.string().optional(),
  company_email: zod.string().email().optional(),
  company_logo: zod.string().url().optional(),
  notes: zod.string().optional(),
})

const InvoiceConfigPage = () => {
  // TODO add implementation
}

export const config = defineRouteConfig({
  label: "Default Invoice Config",
})

export default InvoiceConfigPage
```

A settings page file must export:

1. A React component that renders the page. This is the file's default export.
2. A configuration object created with the `defineRouteConfig` function. It accepts an object with properties that define the page's configuration, such as its sidebar label.

So far, the React component doesn't have any implementation. You also define a Zod schema that you'll use to validate the form data.

Change the implementation of the `InvoiceConfigPage` component to the following:

```tsx title="src/admin/routes/settings/invoice-config/page.tsx" highlights={InvoiceConfigPageHighlights}
const InvoiceConfigPage = () => {
  const { data, isLoading, refetch } = useQuery<{
    invoice_config: InvoiceConfig
  }>({
    queryFn: () => sdk.client.fetch("/admin/invoice-config"),
    queryKey: ["invoice-config"],
  })
  const { mutateAsync, isPending } = useMutation({
    mutationFn: (payload: zod.infer<typeof schema>) => 
      sdk.client.fetch("/admin/invoice-config", {
        method: "POST",
        body: payload,
      }),
    onSuccess: () => {
      refetch()
      toast.success("Invoice config updated successfully")
    },
  })

  const getFormDefaultValues = useCallback(() => {
    return {
      company_name: data?.invoice_config.company_name || "",
      company_address: data?.invoice_config.company_address || "",
      company_phone: data?.invoice_config.company_phone || "",
      company_email: data?.invoice_config.company_email || "",
      company_logo: data?.invoice_config.company_logo || "",
      notes: data?.invoice_config.notes || "",
    }
  }, [data])

  const form = useForm<zod.infer<typeof schema>>({
    defaultValues: getFormDefaultValues(),
  })

  const handleSubmit = form.handleSubmit((formData) => mutateAsync(formData))

  const uploadLogo = async (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0]
    if (!file) {
      return
    }

    const { files } = await sdk.admin.upload.create({
      files: [file],
    })

    form.setValue("company_logo", files[0].url)
  }

  useEffect(() => {
    form.reset(getFormDefaultValues())
  }, [getFormDefaultValues])


  // TODO render form
}
```

In the component, you:

1. Fetch the current invoice configuration using the `useQuery` hook from Tanstack Query and the JS SDK.
2. Define a mutation using the `useMutation` hook to update the invoice configuration when the form is submitted.
3. Define a function that returns the current invoice configuration data, which is useful to create the form.
4. Define a `form` instance with `useForm` from `react-hook-form`. You pass it the Zod validation schema as a type argument, and the default values using the `getFormDefaultValues` function.
5. Define a `handleSubmit` function that updates the invoice configuration using the mutation.
6. Define an `uploadLogo` function that uploads a logo using Medusa's [Upload API route](https://docs.medusajs.com/api/admin#uploads_postuploads).
7. Reset the form's default values when the `data` changes, ensuring the form reflects the latest invoice configuration.

Finally, replace the `TODO` comment with the following `return` statement:

```tsx title="src/admin/routes/settings/invoice-config/page.tsx"
return (
  <Container className="divide-y p-0">
    <div className="flex items-center justify-between px-6 py-4">
      <Heading level="h1">Invoice Config</Heading>
    </div>
    <FormProvider {...form}>
      <form 
        onSubmit={handleSubmit}
        className="flex h-full flex-col overflow-hidden p-2 gap-2"
      >
        <Controller
          control={form.control}
          name="company_name"
          render={({ field }) => {
            return (
              <div className="flex flex-col space-y-2">
                <div className="flex items-center gap-x-1">
                  <Label size="small" weight="plus">
                    Company Name
                  </Label>
                </div>
                <Input {...field} onChange={field.onChange} value={field.value} />
              </div>
            )
          }}
        />
        <Controller
          control={form.control}
          name="company_address"
          render={({ field }) => {
            return (
              <div className="flex flex-col space-y-2">
                <div className="flex items-center gap-x-1">
                  <Label size="small" weight="plus">
                    Company Address
                  </Label>
                </div>
                <Textarea {...field} />
              </div>
            )
          }}
        />
        <Controller
          control={form.control}
          name="company_phone"
          render={({ field }) => {
            return (
              <div className="flex flex-col space-y-2">
                <div className="flex items-center gap-x-1">
                  <Label size="small" weight="plus">
                    Company Phone
                  </Label>
                </div>
                <Input {...field} />
              </div>
            )
          }}
        />
        <Controller
          control={form.control}
          name="company_email"
          render={({ field }) => {
            return (
              <div className="flex flex-col space-y-2">
                <div className="flex items-center gap-x-1">
                  <Label size="small" weight="plus">
                    Company Email
                  </Label>
                </div>
                <Input {...field} />
              </div>
            )
          }}
        />
        <Controller
          control={form.control}
          name="notes"
          render={({ field }) => {
            return (
              <div className="flex flex-col space-y-2">
                <div className="flex items-center gap-x-1">
                  <Label size="small" weight="plus">
                    Notes
                  </Label>
                </div>
                <Textarea {...field} />
              </div>
            )
          }}
        />
        <Controller
          control={form.control}
          name="company_logo"
          render={({ field }) => {
            return (
              <div className="flex flex-col space-y-2">
                <div className="flex items-center gap-x-1">
                  <Label size="small" weight="plus">
                    Company Logo
                  </Label>
                </div>
                <Input type="file" onChange={uploadLogo} className="py-1" />
                {field.value && (
                  <img
                    src={field.value}
                    alt="Company Logo"
                    className="mt-2 h-24 w-24"
                  />
                )}
              </div>
            )
          }}
        />
        <Button type="submit" disabled={isLoading || isPending}>
          Save
        </Button>
      </form>
    </FormProvider>
  </Container>
) 
```

You render the form and its fields. When the form is submitted, you execute the `handleSubmit` function to update the invoice configuration.

Refer to the [Admin Components](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/forms/index.html.md) documentation to learn more about creating forms in the Medusa Admin dashboard.

### Test Update Invoice Configurations

You'll now test out the invoice configuration management feature.

First, start the Medusa application with the following command:

```bash npm2yarn
npm run dev
```

Then:

1. Open the Medusa Admin dashboard in your browser at `http://localhost:9000/admin` and log in.
2. Go to Settings -> Default Invoice Config.

You can edit any of the configurations and upload a company logo. When you click the Save button, it will update the configurations and display a success message.

![Invoice Configurations Settings Page](https://res.cloudinary.com/dza7lstvk/image/upload/v1753788482/Medusa%20Resources/CleanShot_2025-07-29_at_13.54.11_2x_zs2h9w.png)

***

## Step 5: Implement Invoice Generation

In this step, you'll implement the logic to generate an invoice PDF. Later, you'll execute this logic in a workflow, then expose the functionality in an API route.

You'll implement the invoice generation logic within the Invoice Generator Module's service.

First, install the dependencies needed for PDF generation:

```bash npm2yarn
npm install pdfmake
npm install --save-dev @types/pdfmake
```

Where:

- `pdfmake` is a library for generating PDF documents in JavaScript.
- `@types/pdfmake` provides TypeScript type definitions for `pdfmake`, allowing you to use it with TypeScript without type errors.

After that, add the following imports at the top of `src/modules/invoice-generator/service.ts`:

```ts title="src/modules/invoice-generator/service.ts"
import { Invoice, InvoiceStatus } from "./models/invoice"
import PdfPrinter from "pdfmake"
import { 
  InferTypeOf, 
  OrderDTO, 
  OrderLineItemDTO,
} from "@medusajs/framework/types"
import axios from "axios"
```

`axios` is available in your Medusa project by default, so you don't need to install it.

Next, add the following before the service declaration:

```ts title="src/modules/invoice-generator/service.ts"
const fonts = {
  Helvetica: {
    normal: "Helvetica",
    bold: "Helvetica-Bold",
    italics: "Helvetica-Oblique",
    bolditalics: "Helvetica-BoldOblique",
  },
}

const printer = new PdfPrinter(fonts)

type GeneratePdfParams = {
  order: OrderDTO
  items: OrderLineItemDTO[]
}
```

You define the fonts to use in the PDF document. You can choose other fonts, as described in the [pdfmake documentation](https://pdfmake.github.io/docs/0.1/fonts/standard-14-fonts/#use-in-pdfmake-on-server-side).

Next, you initialize the PDF printer that will be used to generate the invoice PDF. You also define a type with the parameters necessary to generate an invoice.

In the service, you'll first add helper functions that are useful for generating the invoice.

Add the following methods to the `InvoiceGeneratorService` class:

```ts title="src/modules/invoice-generator/service.ts" highlights={generatePdfHighlights}
class InvoiceGeneratorService extends MedusaService({
  InvoiceConfig,
  Invoice,
}) {
  private async formatAmount(amount: number, currency: string): Promise<string> {
    return new Intl.NumberFormat("en-US", {
      style: "currency",
      currency: currency,
    }).format(amount)
  }

  private async imageUrlToBase64(url: string): Promise<string> {
    const response = await axios.get(url, { responseType: "arraybuffer" })
    const base64 = Buffer.from(response.data).toString("base64")
    const mimeType = response.headers["content-type"] || "image/png"
    return `data:${mimeType};base64,${base64}`
  }
}
```

You add two methods:

- `formatAmount`: Formats an amount into a string with its currency, which is useful for displaying amounts in the PDF.
- `imageUrlToBase64`: Converts an image URL to a base64 string. This is necessary because `pdfmake` can't render images from URLs.

Next, you'll add the method that returns the PDF content as expected by `pdfmake`.

Add the following method to the `InvoiceGeneratorService` class:

```ts title="src/modules/invoice-generator/service.ts" collapsibleLines="65-425" expandButtonLabel="Show Full Code" highlights={createInvoiceContentHighlights}
class InvoiceGeneratorService extends MedusaService({
  InvoiceConfig,
  Invoice,
}) {
  // ...
  private async createInvoiceContent(
    params: GeneratePdfParams, 
    invoice: InferTypeOf<typeof Invoice>
  ): Promise<Record<string, any>> {
    // Get invoice configuration
    const invoiceConfigs = await this.listInvoiceConfigs()
    const config = invoiceConfigs[0] || {}

    // Create table for order items
    const itemsTable = [
      [
        { text: "Item", style: "tableHeader" },
        { text: "Quantity", style: "tableHeader" },
        { text: "Unit Price", style: "tableHeader" },
        { text: "Total", style: "tableHeader" },
      ],
      ...(await Promise.all(params.items.map(async (item) => [
        { text: item.title || "Unknown Item", style: "tableRow" },
        { text: item.quantity.toString(), style: "tableRow" },
        { text: await this.formatAmount(
          item.unit_price, 
          params.order.currency_code
        ), style: "tableRow" },
        { text: await this.formatAmount(
          Number(item.total), 
          params.order.currency_code
        ), style: "tableRow" },
      ]))),
    ]

    const invoiceId = `INV-${invoice.display_id.toString().padStart(6, "0")}`
    const invoiceDate = new Date(invoice.created_at).toLocaleDateString()

    // return the PDF content structure
    return {
      pageSize: "A4",
      pageMargins: [40, 60, 40, 60],
      header: {
        margin: [40, 20, 40, 0],
        columns: [
          /** Company Logo */
          {
            width: "*",
            stack: [
              ...(config.company_logo ? [
                {
                  image: await this.imageUrlToBase64(config.company_logo),
                  width: 80,
                  height: 40,
                  fit: [80, 40],
                  margin: [0, 0, 0, 10],
                },
              ] : []),
              {
                text: config.company_name || "Your Company Name",
                style: "companyName",
                margin: [0, 0, 0, 0],
              },
            ],
          },
          /** Invoice Title */
          {
            width: "auto",
            stack: [
              {
                text: "INVOICE",
                style: "invoiceTitle",
                alignment: "right",
                margin: [0, 0, 0, 0],
              },
            ],
          },
        ],
      },
      content: [
        {
          margin: [0, 20, 0, 0],
          columns: [
            /** Company Details */
            {
              width: "*",
              stack: [
                {
                  text: "COMPANY DETAILS",
                  style: "sectionHeader",
                  margin: [0, 0, 0, 8],
                },
                config.company_address && {
                  text: config.company_address,
                  style: "companyAddress",
                  margin: [0, 0, 0, 4],
                },
                config.company_phone && {
                  text: config.company_phone,
                  style: "companyContact",
                  margin: [0, 0, 0, 4],
                },
                config.company_email && {
                  text: config.company_email,
                  style: "companyContact",
                  margin: [0, 0, 0, 0],
                },
              ],
            },
            /** Invoice Details */
            {
              width: "auto",
              table: {
                widths: [80, 120],
                body: [
                  [
                    { text: "Invoice ID:", style: "label" },
                    { text: invoiceId, style: "value" },
                  ],
                  [
                    { text: "Invoice Date:", style: "label" },
                    { text: invoiceDate, style: "value" },
                  ],
                  [
                    { text: "Order ID:", style: "label" },
                    { 
                      text: params.order.display_id.toString().padStart(6, "0"), 
                      style: "value",
                    },
                  ],
                  [
                    { text: "Order Date:", style: "label" },
                    { 
                      text: new Date(params.order.created_at).toLocaleDateString(), 
                      style: "value",
                    },
                  ],
                ],
              },
              layout: "noBorders",
              margin: [0, 0, 0, 20],
            },
          ],
        },
        {
          text: "\n",
        },
        /** Billing and Shipping Addresses */
        {
          columns: [
            {
              width: "*",
              stack: [
                {
                  text: "BILL TO",
                  style: "sectionHeader",
                  margin: [0, 0, 0, 8],
                },
                {
                  text: params.order.billing_address ? 
                    `${params.order.billing_address.first_name || ""} ${params.order.billing_address.last_name || ""}
                    ${params.order.billing_address.address_1 || ""}${params.order.billing_address.address_2 ? `\n${params.order.billing_address.address_2}` : ""}
                    ${params.order.billing_address.city || ""}, ${params.order.billing_address.province || ""} ${params.order.billing_address.postal_code || ""}
                    ${params.order.billing_address.country_code || ""}${params.order.billing_address.phone ? `\n${params.order.billing_address.phone}` : ""}` : 
                    "No billing address provided",
                  style: "addressText",
                },
              ],
            },
            {
              width: "*",
              stack: [
                {
                  text: "SHIP TO",
                  style: "sectionHeader",
                  margin: [0, 0, 0, 8],
                },
                {
                  text: params.order.shipping_address ? 
                    `${params.order.shipping_address.first_name || ""} ${params.order.shipping_address.last_name || ""}
                    ${params.order.shipping_address.address_1 || ""} ${params.order.shipping_address.address_2 ? `\n${params.order.shipping_address.address_2}` : ""}
                    ${params.order.shipping_address.city || ""}, ${params.order.shipping_address.province || ""} ${params.order.shipping_address.postal_code || ""}
                    ${params.order.shipping_address.country_code || ""}${params.order.shipping_address.phone ? `\n${params.order.shipping_address.phone}` : ""}` : 
                    "No shipping address provided",
                  style: "addressText",
                },
              ],
            },
          ],
        },
        {
          text: "\n\n",
        },
        /** Items Table */
        {
          table: {
            headerRows: 1,
            widths: ["*", "auto", "auto", "auto"],
            body: itemsTable,
          },
          layout: {
            fillColor: function (rowIndex: number) {
              return (rowIndex === 0) ? "#f8f9fa" : null
            },
            hLineWidth: function (i: number, node: any) {
              return (i === 0 || i === node.table.body.length) ? 0.8 : 0.3
            },
            vLineWidth: function (i: number, node: any) {
              return 0.3
            },
            hLineColor: function (i: number, node: any) {
              return (i === 0 || i === node.table.body.length) ? "#cbd5e0" : "#e2e8f0"
            },
            vLineColor: function () {
              return "#e2e8f0"
            },
            paddingLeft: function () {
              return 8
            },
            paddingRight: function () {
              return 8
            },
            paddingTop: function () {
              return 6
            },
            paddingBottom: function () {
              return 6
            },
          },
        },
        {
          text: "\n",
        },
        /** Totals Section */
        {
          columns: [
            { width: "*", text: "" },
            {
              width: "auto",
              table: {
                widths: ["auto", "auto"],
                body: [
                  [
                    { text: "Subtotal:", style: "totalLabel" },
                    { 
                      text: await this.formatAmount(
                        Number(params.order.subtotal), 
                        params.order.currency_code), 
                      style: "totalValue",
                    },
                  ],
                  [
                    { text: "Tax:", style: "totalLabel" },
                    { 
                      text: await this.formatAmount(
                        Number(params.order.tax_total), 
                        params.order.currency_code), 
                      style: "totalValue",
                    },
                  ],
                  [
                    { text: "Shipping:", style: "totalLabel" },
                    { 
                      text: await this.formatAmount(
                        Number(params.order.shipping_methods?.[0]?.total || 0), 
                        params.order.currency_code), 
                      style: "totalValue",
                    },
                  ],
                  [
                    { text: "Discount:", style: "totalLabel" },
                    { 
                      text: await this.formatAmount(
                        Number(params.order.discount_total), 
                        params.order.currency_code), 
                      style: "totalValue",
                    },
                  ],
                  [
                    { text: "Total:", style: "totalLabel" },
                    { 
                      text: await this.formatAmount(
                        Number(params.order.total), 
                        params.order.currency_code), 
                      style: "totalValue",
                    },
                  ],
                ],
              },
              layout: {
                fillColor: function (rowIndex: number) {
                  return (rowIndex === 3) ? "#f8f9fa" : null
                },
                hLineWidth: function (i: number, node: any) {
                  return (i === 0 || i === node.table.body.length) ? 0.8 : 0.3
                },
                vLineWidth: function () {
                  return 0.3
                },
                hLineColor: function (i: number, node: any) {
                  return (i === 0 || i === node.table.body.length) ? "#cbd5e0" : "#e2e8f0"
                },
                vLineColor: function () {
                  return "#e2e8f0"
                },
                paddingLeft: function () {
                  return 8
                },
                paddingRight: function () {
                  return 8
                },
                paddingTop: function () {
                  return 6
                },
                paddingBottom: function () {
                  return 6
                },
              },
            },
          ],
        },
        {
          text: "\n\n",
        },
        /** Notes Section */
        ...(config.notes ? [
          {
            text: "Notes",
            style: "sectionHeader",
            margin: [0, 20, 0, 10],
          },
          {
            text: config.notes,
            style: "notesText",
            margin: [0, 0, 0, 20],
          },
        ] : []),
        {
          text: "Thank you for your business!",
          style: "thankYouText",
          alignment: "center",
          margin: [0, 30, 0, 0],
        },
      ],
      styles: {
        companyName: {
          fontSize: 22,
          bold: true,
          color: "#1a365d",
          margin: [0, 0, 0, 5],
        },
        companyAddress: {
          fontSize: 11,
          color: "#4a5568",
          lineHeight: 1.3,
        },
        companyContact: {
          fontSize: 10,
          color: "#4a5568",
        },
        invoiceTitle: {
          fontSize: 24,
          bold: true,
          color: "#2c3e50",
        },
        label: {
          fontSize: 10,
          color: "#6c757d",
          margin: [0, 0, 8, 0],
        },
        value: {
          fontSize: 10,
          bold: true,
          color: "#2c3e50",
        },
        sectionHeader: {
          fontSize: 12,
          bold: true,
          color: "#2c3e50",
          backgroundColor: "#f8f9fa",
          padding: [8, 12],
        },
        addressText: {
          fontSize: 10,
          color: "#495057",
          lineHeight: 1.3,
        },
        tableHeader: {
          fontSize: 10,
          bold: true,
          color: "#ffffff",
          fillColor: "#495057",
        },
        tableRow: {
          fontSize: 9,
          color: "#495057",
        },
        totalLabel: {
          fontSize: 10,
          bold: true,
          color: "#495057",
        },
        totalValue: {
          fontSize: 10,
          bold: true,
          color: "#2c3e50",
        },
        notesText: {
          fontSize: 10,
          color: "#6c757d",
          italics: true,
          lineHeight: 1.4,
        },
        thankYouText: {
          fontSize: 12,
          color: "#28a745",
          italics: true,
        },
      },
      defaultStyle: {
        font: "Helvetica",
      },
    }
  }
}
```

This method returns a [pdfmake document definition](https://pdfmake.github.io/docs/0.1/document-definition-object/) object that describes the structure and content of the invoice PDF.

You show in the PDF:

- Company details, including the logo, name, address, phone, and email.
- Invoice details, such as the invoice ID, date, order ID, and order date.
- Billing and shipping addresses.
- A table with the order items, including item name, quantity, unit price, and total.
- A totals section that summarizes the subtotal, tax, shipping, discount, and total amounts.
- An optional notes section for additional information.

You can customize the styles, layout, and content as needed to match your branding and requirements. Refer to the [pdfmake documentation](https://pdfmake.github.io/docs/0.1/document-definition-object/) to learn about available content types.

Finally, you'll add the method that generates the PDF and returns it as a buffer.

Add the following method to the `InvoiceGeneratorService` class:

```ts title="src/modules/invoice-generator/service.ts" highlights={generatePdfMethodHighlights}
class InvoiceGeneratorService extends MedusaService({
  InvoiceConfig,
  Invoice,
}) {
  // ...
  async generatePdf(params: GeneratePdfParams & {
    invoice_id: string
  }): Promise<Buffer> {
    const invoice = await this.retrieveInvoice(params.invoice_id)

    // Generate new content
    const pdfContent = Object.keys(invoice.pdfContent).length ? 
      invoice.pdfContent : 
      await this.createInvoiceContent(params, invoice)

    await this.updateInvoices({
      id: invoice.id,
      pdfContent,
    })

    // get PDF as a Buffer
    return new Promise((resolve, reject) => {
      const chunks: Buffer[] = []
  
      const pdfDoc = printer.createPdfKitDocument(pdfContent as any)
      
      pdfDoc.on("data", (chunk) => chunks.push(chunk))
      pdfDoc.on("end", () => {
        const result = Buffer.concat(chunks)
        resolve(result)
      })
      pdfDoc.on("error", (err) => reject(err))
  
      pdfDoc.end() // Finalize PDF stream
    })
  }
}
```

In this method, you receive the ID of the invoice to generate its PDF, along with the order details.

You either retrieve its existing content, or generate new content. Finally, you return the PDF invoice as a Buffer.

You'll test out this functionality in the next step.

***

## Step 6: Generate Invoice PDF Workflow and API Routes

In this step, you'll create a workflow that generates an invoice PDF using the `InvoiceGeneratorService` you implemented in the previous step. Then, you'll create API routes for admin users and customers to download the generated invoice PDF.

### a. Generate Invoice PDF Workflow

The workflow that generates the invoice PDF will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve order details
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve countries for billing and shipping addresses
- [getOrderInvoiceStep](#getOrderInvoiceStep): Retrieve the invoice to generate its PDF.
- [generateInvoicePdfStep](#generateInvoicePdfStep): Generate invoice PDF

You only need to implement the third and fourth steps, as the other two steps are helper steps that Medusa provides.

#### getOrderInvoiceStep

The `getOrderInvoiceStep` will either retrieve an existing invoice for an order, or create a new one.

Create the file `src/workflows/steps/get-order-invoice.ts` with the following content:

```ts title="src/workflows/steps/get-order-invoice.ts" highlights={getOrderInvoiceStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { INVOICE_MODULE } from "../../modules/invoice-generator"
import { InvoiceStatus } from "../../modules/invoice-generator/models/invoice"

type StepInput = {
  order_id: string
}

export const getOrderInvoiceStep = createStep(
  "get-order-invoice",
  async ({ order_id }: StepInput, { container }) => {
    const invoiceGeneratorService = container.resolve(INVOICE_MODULE)
    let [invoice] = await invoiceGeneratorService.listInvoices({
      order_id,
      status: InvoiceStatus.LATEST,
    })
    let createdInvoice = false

    if (!invoice) {
      // Store new invoice in database
      invoice = await invoiceGeneratorService.createInvoices({
        order_id,
        status: InvoiceStatus.LATEST,
        pdfContent: {},
      })
      createdInvoice = true
    }

    return new StepResponse(invoice, {
      created_invoice: createdInvoice,
      invoice_id: invoice.id,
    })
  },
  async (data, { container }) => {
    const { created_invoice, invoice_id } = data || {}
    if (!created_invoice || !invoice_id) {
      return
    }
    const invoiceGeneratorService = container.resolve(INVOICE_MODULE)

    invoiceGeneratorService.deleteInvoices(invoice_id)
  }
)
```

In the step, you try to retrieve an existing invoice with the `latest` status for the given order ID. If none exist, you create a new invoice.

The step returns the invoice. In the compensation function, you delete the invoice if an error occurs during the workflow execution.

#### generateInvoicePdfStep

The `generateInvoicePdfStep` will generate and return the invoice PDF as a buffer.

Create the file `src/workflows/steps/generate-invoice-pdf.ts` with the following content:

```ts title="src/workflows/steps/generate-invoice-pdf.ts" highlights={generateInvoicePdfStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { INVOICE_MODULE } from "../../modules/invoice-generator"
import { OrderDTO, OrderLineItemDTO } from "@medusajs/framework/types"

export type GenerateInvoicePdfStepInput = {
  order: OrderDTO
  items: OrderLineItemDTO[]
  invoice_id: string
}

export const generateInvoicePdfStep = createStep(
  "generate-invoice-pdf",
  async (input: GenerateInvoicePdfStepInput, { container }) => {
    const invoiceGeneratorService = container.resolve(INVOICE_MODULE)

    const previousInv = await invoiceGeneratorService.retrieveInvoice(
      input.invoice_id
    )

    const pdfBuffer = await invoiceGeneratorService.generatePdf({
      order: input.order,
      items: input.items,
      invoice_id: input.invoice_id,
    })

    return new StepResponse({
      pdf_buffer: pdfBuffer,
    }, previousInv)
  },
  async (previousInv, { container }) => {
    if (!previousInv) {
      return
    }

    const invoiceGeneratorService = container.resolve(INVOICE_MODULE)

    await invoiceGeneratorService.updateInvoices({
      id: previousInv.id,
      pdfContent: previousInv.pdfContent,
    })
  }
)
```

In the step, you first retrieve the invoice's data before generating the PDF. Then, you generate the PDF and return it as a buffer.

In the compensation function, you update the invoice with the previous PDF content if an error occurs during the workflow execution.

#### Generate Invoice Workflow

To create the workflow that generates invoices, create the file `src/workflows/generate-invoice-pdf.ts` with the following content:

```ts title="src/workflows/generate-invoice-pdf.ts" highlights={generateInvoicePdfWorkflowHighlights}
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { generateInvoicePdfStep, GenerateInvoicePdfStepInput } from "./steps/generate-invoice-pdf"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { getOrderInvoiceStep } from "./steps/get-order-invoice"

type WorkflowInput = {
  order_id: string
}

export const generateInvoicePdfWorkflow = createWorkflow(
  "generate-invoice-pdf",
  (input: WorkflowInput) => {
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "id",
        "display_id",
        "created_at",
        "currency_code",
        "total",
        "items.*",
        "items.variant.*",
        "items.variant.product.*",
        "shipping_address.*",
        "billing_address.*",
        "shipping_methods.*",
        "tax_total",
        "subtotal",
        "discount_total",
      ],
      filters: {
        id: input.order_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })
    const countryFilters = transform({
      orders,
    }, (data) => {
      const country_codes: string[] = []
      if (data.orders[0].billing_address?.country_code) {
        country_codes.push(data.orders[0].billing_address.country_code)
      }
      if (data.orders[0].shipping_address?.country_code) {
        country_codes.push(data.orders[0].shipping_address.country_code)
      }
      return country_codes
    })
    const { data: countries } = useQueryGraphStep({
      entity: "country",
      fields: ["display_name", "iso_2"],
      filters: {
        iso_2: countryFilters,
      },
    }).config({ name: "retrieve-countries" })

    const transformedOrder = transform({
      orders,
      countries,
    }, (data) => {
      const order = data.orders[0]
      
      if (order.billing_address?.country_code) {
        order.billing_address.country_code = data.countries.find(
          (country) => country.iso_2 === order.billing_address!.country_code
        )?.display_name || order.billing_address!.country_code
      }
      
      if (order.shipping_address?.country_code) {
        order.shipping_address.country_code = data.countries.find(
          (country) => country.iso_2 === order.shipping_address!.country_code
        )?.display_name || order.shipping_address!.country_code
      }

      return order
    })

    const invoice = getOrderInvoiceStep({
      order_id: transformedOrder.id,
    })

    const { pdf_buffer } = generateInvoicePdfStep({
      order: transformedOrder,
      items: transformedOrder.items,
      invoice_id: invoice.id,
    } as unknown as GenerateInvoicePdfStepInput)

    return new WorkflowResponse({
      pdf_buffer,
    })
  }
)
```

In the workflow, you:

1. Retrieve the order details using `useQueryGraphStep`.
2. Prepare the country codes filter from the billing and shipping addresses. You'll use this filter to retrieve the display names of the countries.
   - To manipulate data in a workflow, you need to use the `transform` function. Learn more in the [Data Manipulation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) documentation.
3. Retrieve the country details, including the display names, using `useQueryGraphStep`.
4. Transform the order to replace country codes with display names.
5. Retrieve or create the invoice using the `getOrderInvoiceStep`.
6. Generate the invoice PDF using the `generateInvoicePdfStep`.
7. Return the PDF buffer.

### b. Generate Invoice Admin API Route

Next, you'll create an API route that allows admin users to download an order's invoice.

Create the file `src/api/admin/orders/[id]/invoices/route.ts` with the following content:

```ts title="src/api/admin/orders/[id]/invoices/route.ts" highlights={adminInvoiceHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
import { generateInvoicePdfWorkflow } from "../../../../../workflows/generate-invoice-pdf"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
): Promise<void> {
  const { id } = req.params

  const { result: {
    pdf_buffer,
  } } = await generateInvoicePdfWorkflow(req.scope)
    .run({
      input: {
        order_id: id,
      },
    })

  const buffer = Buffer.from(pdf_buffer)

  res.set({
    "Content-Type": "application/pdf",
    "Content-Disposition": `attachment; filename="invoice-${id}.pdf"`,
    "Content-Length": buffer.length,
  })
  
  res.send(buffer)
} 
```

You export a `GET` route handler function, exposing a `GET` API route at `/admin/orders/:id/invoices`.

In the route handler, you execute the `generateInvoicePdfWorkflow` which will return the PDF buffer.

Workflows and steps serialize their output. So, you must recreate the buffer from the serialized output using `Buffer.from`. Learn more in the [Constructor Constraints](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints#returned-values/index.html.md) documentation.

Finally, you set the response headers to indicate that the response is a PDF file and send the buffer as the response body.

### c. Generate Invoice Store API Route

You'll also create an identical API route for the storefront, allowing customers to download their order invoices.

Create the file `src/api/store/orders/[id]/invoices/route.ts` with the following content:

```ts title="src/api/store/orders/[id]/invoices/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
import { generateInvoicePdfWorkflow } from "../../../../../workflows/generate-invoice-pdf"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
): Promise<void> {
  const { id } = req.params

  const { result: {
    pdf_buffer,
  } } = await generateInvoicePdfWorkflow(req.scope)
    .run({
      input: {
        order_id: id,
      },
    })

  const buffer = Buffer.from(pdf_buffer)

  res.set({
    "Content-Type": "application/pdf",
    "Content-Disposition": `attachment; filename="invoice-${id}.pdf"`,
    "Content-Length": buffer.length,
  })
  
  res.send(buffer)
}
```

You expose a `GET` API route at `/store/orders/:id/invoices` that allows customers to download their order invoices.

You'll test out both API routes in the next steps.

***

## Step 7: Add Admin Widget to Download Invoices

In this step, you'll create an admin widget that allows admin users to download invoices directly from order detail pages.

A [widget](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md) is a React component that is injected into an existing admin page.

To create the widget, create the file `src/admin/widgets/order-invoice.tsx` with the following content:

```tsx title="src/admin/widgets/order-invoice.tsx" highlights={orderInvoiceWidgetHighlights1}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Button, Container, Heading, Text, toast } from "@medusajs/ui"
import { AdminOrder, DetailWidgetProps } from "@medusajs/framework/types"
import { sdk } from "../lib/sdk"
import { useState } from "react"

const OrderInvoiceWidget = ({ data: order }: DetailWidgetProps<AdminOrder>) => {
  // TODO implement widget
}

export const config = defineWidgetConfig({
  zone: "order.details.side.before",
})

export default OrderInvoiceWidget 
```

A widget file must export:

1. A React component that contains the widget UI. It's the default export of the file.
2. A widget configuration object that defines where the widget should be injected in the Medusa Admin.

Next, you'll add the implementation of the widget. Replace the implementation of the `OrderInvoiceWidget` component with the following code:

```tsx title="src/admin/widgets/order-invoice.tsx" highlights={orderInvoiceWidgetHighlights2}
const OrderInvoiceWidget = ({ data: order }: DetailWidgetProps<AdminOrder>) => {
  const [isDownloading, setIsDownloading] = useState(false)

  const downloadInvoice = async () => {
    setIsDownloading(true)
    
    try {
      const response: Response = await sdk.client.fetch(
        `/admin/orders/${order.id}/invoices`, 
        {
          method: "GET",
          headers: {
            "accept": "application/pdf",
          },
        }
      )
  
      const blob = await response.blob()
      const url = window.URL.createObjectURL(blob)
      const a = document.createElement("a")
      a.href = url
      a.download = `invoice-${order.id}.pdf`
      document.body.appendChild(a)
      a.click()
      window.URL.revokeObjectURL(url)
      document.body.removeChild(a)
      setIsDownloading(false)
      toast.success("Invoice generated and downloaded successfully")
    } catch (error) {
      toast.error(`Failed to generate invoice: ${error}`)
      setIsDownloading(false)
    }
  }

  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <div>
          <Heading level="h2">Invoice</Heading>
          <Text size="small" className="text-ui-fg-subtle">
            Generate and download invoice for this order
          </Text>
        </div>
      </div>

      <div className="flex items-center justify-end px-6 py-4">
        <Button
          variant="secondary"
          disabled={isDownloading}
          onClick={downloadInvoice}
          isLoading={isDownloading}
        >
          Download Invoice
        </Button>
      </div>
    </Container>
  )
}
```

The widget receives the order's data as a prop since it's injected into the order detail page.

In the widget, you define a `downloadInvoice` function that retrieves the PDF from the `/admin/orders/:id/invoices` API route and then triggers the download of the PDF.

You return a container with a button to download the invoice.

### Test Admin Widget

To test the admin widget, start the Medusa Admin application again.

Next, start the Next.js Starter Storefront to easily place an order:

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-invoice`, you can find the storefront by going back to the parent directory and changing to the `medusa-invoice-storefront` directory:

```bash
cd ../medusa-invoice-storefront # change based on your project name
```

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

Open the storefront at `http://localhost:8000`. Add a product to the cart, then place an order.

In the Medusa Admin, open the order's detail page. You'll see an "Invoice" section at the top right of the page. Click on the "Download Invoice" button to generate and download the invoice PDF.

![Download Invoice Button in Admin Widget](https://res.cloudinary.com/dza7lstvk/image/upload/v1753788505/Medusa%20Resources/CleanShot_2025-07-29_at_14.27.08_2x_rnn1er.png)

You can view the downloaded invoice PDF to make sure all information is correct.

#### Tip: Making Changes to the Invoice Generator

After the first time the invoice is generated, the `generatePdf` method will use that same invoice content to avoid generating the content every time.

If you make changes to the `createInvoiceContent` method and you want to test it out, you'll have to place a new order for now. In a [later step](#step-10-mark-invoices-as-stale-on-order-updates), you'll implement the functionality to mark an invoice as stale when an order is updated. This will allow you to easily test out changes to the invoice generation without placing a new order.

***

## Step 8: Download Invoice in Storefront

In this step, you'll customize the Next.js Starter Storefront to allow customers to download an order's invoice either from its confirmation or detail pages.

The `OrderDetails` component available in `src/modules/order/components/order-details/index.tsx` is used on both the order confirmation and detail pages. So, you only need to customize this component.

First, add the following at the top of the file:

```tsx title="src/modules/order/components/order-details/index.tsx" badgeLabel="Storefront" badgeColor="blue"
"use client"

// other imports...
import { Button, toast } from "@medusajs/ui"
import { useState } from "react"
import { sdk } from "../../../../lib/config"
```

You make the component a [client component](https://react.dev/reference/rsc/use-client) and you add the necessary imports.

Next, add the following variable and function in the `OrderDetails` component:

```tsx title="src/modules/order/components/order-details/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={orderDetailsHighlights}
const OrderDetails = ({ order, showStatus }: OrderDetailsProps) => {
  const [isDownloading, setIsDownloading] = useState(false)

  const downloadInvoice = async () => {
    setIsDownloading(true)
    
    try {
      const response: Response = await sdk.client.fetch(
        `/store/orders/${order.id}/invoices`, 
        {
          method: "GET",
          headers: {
            "accept": "application/pdf",
          },
        }
      )
  
      const blob = await response.blob()
      const url = window.URL.createObjectURL(blob)
      const a = document.createElement("a")
      a.href = url
      a.download = `invoice-${order.id}.pdf`
      document.body.appendChild(a)
      a.click()
      window.URL.revokeObjectURL(url)
      document.body.removeChild(a)
      setIsDownloading(false)
      toast.success("Invoice generated and downloaded successfully")
    } catch (error) {
      toast.error(`Failed to generate invoice: ${error}`)
      setIsDownloading(false)
    }
  }

  // ...
}
```

You define an `isDownloading` state variable to manage the download state and a `downloadInvoice` function that retrieves the PDF from the `/store/orders/:id/invoices` API route and triggers the download.

Finally, in the `return` statement, find the following lines:

```tsx title="src/modules/order/components/order-details/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"], ["5"], ["6"]]}
return (
  <div>
    {/* ... */}
    <Text className="mt-2 text-ui-fg-interactive">
      Order number: <span data-testid="order-id">{order.display_id}</span>
    </Text>
    {/* ... */}
  </div>
)
```

And replace them with the following:

```tsx title="src/modules/order/components/order-details/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"], ["5"], ["6"], ["7"], ["8"], ["9"], ["10"], ["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]}
return (
  <div>
    {/* ... */}
    <div className="flex gap-2 items-center mt-2">
      <Text className="text-ui-fg-interactive">
        Order number: <span data-testid="order-id">{order.display_id}</span>
      </Text>
      <Button 
        variant="secondary" 
        onClick={downloadInvoice} 
        disabled={isDownloading} 
        isLoading={isDownloading}
      >
        Download Invoice
      </Button>
    </div>
    {/* ... */}
  </div>
)
```

You show the "Download Invoice" button next to the order number. When the customer clicks the button, it triggers the `downloadInvoice` function to download the invoice PDF.

### Test Storefront Invoice Download

To test the storefront changes, ensure both the Medusa application and the Next.js Starter Storefront are running.

Then, place an order in the storefront. You'll see the "Download Invoice" button on the order confirmation page. You can click the button to generate and download the invoice PDF.

![Download Invoice Button in Storefront](https://res.cloudinary.com/dza7lstvk/image/upload/v1753789117/Medusa%20Resources/CleanShot_2025-07-29_at_14.38.21_2x_oyydri.png)

***

## Step 9: Send Invoice in Email Notifications

In this step, you'll send an order confirmation email with the invoice PDF attached when an order is placed.

You can listen to events that occur in your Medusa application, such as when an order is placed, using [subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). A subscriber is an asynchronous function that is executed whenever its associated event is emitted.

To create a subscriber, create the file `src/subscribers/order-placed.ts` with the following content:

```ts title="src/subscribers/order-placed.ts" highlights={orderPlacedHighlights}
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { generateInvoicePdfWorkflow } from "../workflows/generate-invoice-pdf"

export default async function orderPlacedHandler({
  event: { data },
  container,
}: SubscriberArgs<{
  id: string
}>) {
  const query = container.resolve("query")
  const notificationModuleService = container.resolve("notification")

  const { data: [order] } = await query.graph({
    entity: "order",
    fields: [
      "id",
      "display_id",
      "created_at",
      "currency_code",
      "total",
      "email",
      "items.*",
      "items.variant.*",
      "items.variant.product.*",
      "shipping_address.*",
      "billing_address.*",
      "shipping_methods.*",
      "tax_total",
      "subtotal",
      "discount_total",
      // add any other fields you need for the email template...
    ],
    filters: {
      id: data.id,
    },
  })

  const { result: {
    pdf_buffer,
  } } = await generateInvoicePdfWorkflow(container)
    .run({
      input: {
        order_id: data.id,
      },
    })

  const buffer = Buffer.from(pdf_buffer)

  // Convert to binary string to pass as attachment
  const binaryString = [...buffer]
    .map((byte) => byte.toString(2).padStart(8, "0"))
    .join("")

  await notificationModuleService.createNotifications({
    to: order.email || "",
    template: "order-placed",
    channel: "email",
    data: order,
    attachments: [
      {
        content: binaryString,
        filename: `invoice-${order.id}.pdf`,
        content_type: "application/pdf",
        disposition: "attachment",
      },
    ],
  })
}

export const config: SubscriberConfig = {
  event: "order.placed",
}
```

A subscriber file must export:

- An asynchronous function, which is the subscriber function that is executed when the event is emitted.
- A configuration object that defines the event the subscriber listens to.

In the subscriber, you:

- Use Query to retrieve the order details. These details are useful to pass to the notification template.
- Generate the invoice PDF using the `generateInvoicePdfWorkflow`.
- Convert the PDF buffer to a binary string, which is required for the email attachment.
- Send an email using the [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) with the order details and the invoice PDF attached.

#### Notification Module Provider to Use

Since the notification's channel is `email`, you need a Notification Module Provider that supports sending emails, such as [SendGrid](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md) or [Resend](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/integrations/guides/resend/index.html.md).

After setting up the Notification Module provider, make sure to replace the `template` of the notification with the template ID from the provider:

```ts title="src/subscribers/order-placed.ts"
await notificationModuleService.createNotifications({
  template: "your-template-id", // Replace with your actual template ID
  // ...
})
```

Alternatively, for testing purposes, you can change the channel to `feed`. This will only log the notification to the console instead of sending an email:

```ts title="src/subscribers/order-placed.ts"
await notificationModuleService.createNotifications({
  channel: "feed", // Change for testing
  // ...
})
```

### Test Order Confirmation Email with Invoice

To test the order confirmation email, make sure that the Medusa application and the Next.js Starter Storefront are running.

Then, place an order in the storefront. You'll see the following message in the Medusa application's console:

```bash
info:    Processing order.placed which has 1 subscribers
```

The notification will either be sent to the customer's email address, or logged to the console if you set the channel to `feed`.

***

## Step 10: Mark Invoices as Stale on Order Updates

When an order is updated, you need to mark its existing invoice as stale so that the next time an invoice is generated, it includes the updated content. This ensures that admin users and customers always have access to an up-to-date invoice.

In this step, you'll create a workflow that marks an order's existing invoices as stale. Then, you'll execute the workflow in a subscriber that runs whenever an order is updated.

### a. Mark Invoices as Stale Workflow

The workflow that marks invoices as stale will have only a single step that updates the invoice status in the database.

#### Update Invoices Step

To create the step that updates the status of invoices, create the file `src/workflows/steps/update-invoices.ts` with the following content:

```ts title="src/workflows/steps/update-invoices.ts" highlights={updateInvoicesStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { InvoiceStatus } from "../../modules/invoice-generator/models/invoice"
import { INVOICE_MODULE } from "../../modules/invoice-generator"

type StepInput = {
  selector: {
    order_id: string
  }
  data: {
    status: InvoiceStatus
  }
}

export const updateInvoicesStep = createStep(
  "update-invoices",
  async ({ selector, data }: StepInput, { container }) => {
    const invoiceGeneratorService = container.resolve(INVOICE_MODULE)

    const prevData = await invoiceGeneratorService.listInvoices(
      selector
    )

    const updatedInvoices = await invoiceGeneratorService.updateInvoices({
      selector,
      data,
    })

    return new StepResponse(updatedInvoices, prevData)
  },
  async (prevData, { container }) => {
    if (!prevData) {
      return
    }

    const invoiceGeneratorService = container.resolve(INVOICE_MODULE)

    await invoiceGeneratorService.updateInvoices(
      prevData.map((i) => ({
        id: i.id,
        status: i.status,
      }))
    )
  }
)
```

In the step, you retrieve the invoices that belong to an order and update their status to `stale`. In the compensation function, you revert the status of the invoices back to their previous state in case the workflow execution fails.

The step returns the updated invoices.

#### Mark Invoices Stale Workflow

Next, to create the workflow that marks invoices as stale, create the file `src/workflows/mark-invoices-stale.ts` with the following content:

```ts title="src/workflows/mark-invoices-stale.ts"
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { updateInvoicesStep } from "./steps/update-invoices"
import { InvoiceStatus } from "../modules/invoice-generator/models/invoice"

type WorkflowInput = {
  order_id: string
}

export const markInvoicesStaleWorkflow = createWorkflow(
  "mark-invoices-stale",
  (input: WorkflowInput) => {
    const updatedInvoices = updateInvoicesStep({
      selector: {
        order_id: input.order_id,
      },
      data: {
        status: InvoiceStatus.STALE,
      },
    })

    return new WorkflowResponse({
      invoices: updatedInvoices,
    })
  }
)
```

The workflow accepts the ID of the order whose invoices should be marked as stale.

In the workflow, you update the invoices using the `updateInvoicesStep`, and return the updated invoices.

### b. Create Order Updated Subscriber

Next, you'll create a subscriber that listens to order updates and marks the order's invoices as stale.

Create the subscriber `src/subscribers/order-updated.ts` with the following content:

```ts title="src/subscribers/order-updated.ts"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { markInvoicesStaleWorkflow } from "../workflows/mark-invoices-stale"

type EventPayload = {
  id: string
} | {
  order_id: string
}

export default async function orderUpdatedHandler({
  event: { data },
  container,
}: SubscriberArgs<EventPayload>) {
  const orderId = "id" in data ? data.id : data.order_id

  await markInvoicesStaleWorkflow(container)
    .run({
      input: {
        order_id: orderId,
      },
    })
}

export const config: SubscriberConfig = {
  event: [
    "order.updated", 
    "order-edit.confirmed",
    "order.exchange_created",
    "order.claim_created",
    "order.return_received",
  ],
}
```

The subscriber will run whenever any of the following events are emitted:

- `order.updated`: When an order's general details, such as billing or shipping address, are updated.
- `order-edit.confirmed`: When an order edit, which may change the order's items, is confirmed.
- `order.exchange_created`: When an exchange is confirmed for an order, which may change the order's items and totals.
- `order.claim_created`: When a claim is confirmed for an order, which may change the order's items and totals.
- `order.return_received`: When a return is confirmed and received for an order, which may change the order's items and totals.

In the subscriber, you execute the `markInvoicesStaleWorkflow` to mark the order's invoices as stale whenever an order is updated.

Refer to the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for a complete list of events that Medusa emits.

### Test Order Updates

To test the order updates subscriber, start the Medusa application and go to an order's page.

On the order's page, try editing its billing or shipping address, or edit the order's items. You'll see the following message in the console:

```bash
info:    Processing order.updated which has 1 subscribers
```

Afterward, try clicking the "Download Invoice" button. The new invoice will contain the order's updated details.

***

## Next Steps

You've successfully implemented the invoice generator feature in Medusa. You can expand on this feature to:

1. Customize the PDF's content and layout to match your branding or include more details.
2. Show all invoices for an order in the admin and storefront, allowing users to download previous invoices.
3. Trigger sending invoices to customers from the Medusa Admin dashboard.

### Learn More about Medusa

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.


# Implement Loyalty Points System in Medusa

In this tutorial, you'll learn how to implement a loyalty points system in Medusa.

Cloud provides a beta Loyalty Plugin feature that facilitates building a loyalty point system. Refer to the [Cloud Loyalty Plugin](https://docs.medusajs.com/cloud/loyalty-plugin/index.html.md) documentation to learn more.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md), which are available out-of-the-box. These features include management capabilities related to carts, orders, promotions, and more.

A loyalty point system allows customers to earn points for purchases, which can be redeemed for discounts or rewards. In this tutorial, you'll learn how to customize the Medusa application to implement a loyalty points system.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

## Summary

By following this tutorial, you will learn how to:

- Install and set up Medusa.
- Define models to store loyalty points and the logic to manage them.
- Build flows that allow customers to earn and redeem points during checkout.
  - Points are redeemed through dynamic promotions specific to the customer.
- Customize the cart completion flow to validate applied loyalty points.

![Diagram illustrating redeem loyalty points flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1744126213/Medusa%20Resources/redeem-points-flow_kzgkux.jpg)

- [Loyalty Points Repository](https://github.com/medusajs/examples/tree/main/loyalty-points): Find the full code for this guide in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1744212595/OpenApi/Loyalty-Points_jwi5e9.yaml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.

Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Loyalty Module

In Medusa, you can build custom features in a [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

In the module, you define the data models necessary for a feature and the logic to manage these data models. Later, you can build commerce flows around your module.

In this step, you'll build a Loyalty Module that defines the necessary data models to store and manage loyalty points for customers.

Refer to the [Modules documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to learn more.

### Create Module Directory

Modules are created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/loyalty`.

### Create Data Models

A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

Refer to the [Data Models documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md) to learn more.

For the Loyalty Module, you need to define a `LoyaltyPoint` data model that represents a customer's loyalty points. So, create the file `src/modules/loyalty/models/loyalty-point.ts` with the following content:

```ts title="src/modules/loyalty/models/loyalty-point.ts" highlights={dmlHighlights}
import { model } from "@medusajs/framework/utils"

const LoyaltyPoint = model.define("loyalty_point", {
  id: model.id().primaryKey(),
  points: model.number().default(0),
  customer_id: model.text().unique("IDX_LOYALTY_CUSTOMER_ID"), 
})

export default LoyaltyPoint
```

You define the `LoyaltyPoint` data model using the `model.define` method of the DML. It accepts the data model's table name as a first parameter, and the model's schema object as a second parameter.

The `LoyaltyPoint` data model has the following properties:

- `id`: A unique ID for the loyalty points.
- `points`: The number of loyalty points a customer has.
- `customer_id`: The ID of the customer who owns the loyalty points. This property has a unique index to ensure that each customer has only one record in the `loyalty_point` table.

Learn more about defining data model properties in the [Property Types documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md).

### Create Module's Service

You now have the necessary data model in the Loyalty Module, but you'll need to manage its records. You do this by creating a service in the module.

A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, allowing you to manage your data models, or connect to a third-party service, which is useful if you're integrating with external services.

Refer to the [Module Service documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md) to learn more.

To create the Loyalty Module's service, create the file `src/modules/loyalty/service.ts` with the following content:

```ts title="src/modules/loyalty/service.ts"
import { MedusaError, MedusaService } from "@medusajs/framework/utils"
import LoyaltyPoint from "./models/loyalty-point"
import { InferTypeOf } from "@medusajs/framework/types"

type LoyaltyPoint = InferTypeOf<typeof LoyaltyPoint>

class LoyaltyModuleService extends MedusaService({
  LoyaltyPoint,
}) {
  // TODO add methods
}

export default LoyaltyModuleService
```

The `LoyaltyModuleService` extends `MedusaService` from the Modules SDK which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods.

So, the `LoyaltyModuleService` class now has methods like `createLoyaltyPoints` and `retrieveLoyaltyPoint`.

Find all methods generated by the `MedusaService` in [the Service Factory reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md).

#### Add Methods to the Service

Aside from the basic CRUD methods, you need to add methods that handle custom functionalities related to loyalty points.

First, you need a method that adds loyalty points for a customer. Add the following method to the `LoyaltyModuleService`:

```ts title="src/modules/loyalty/service.ts"
class LoyaltyModuleService extends MedusaService({
  LoyaltyPoint,
}) {
  async addPoints(customerId: string, points: number): Promise<LoyaltyPoint> {
    const existingPoints = await this.listLoyaltyPoints({
      customer_id: customerId,
    })

    if (existingPoints.length > 0) {
      return await this.updateLoyaltyPoints({
        id: existingPoints[0].id,
        points: existingPoints[0].points + points,
      })
    }

    return await this.createLoyaltyPoints({
      customer_id: customerId,
      points,
    })
  }
}
```

You add an `addPoints` method that accepts two parameters: the ID of the customer and the points to add.

In the method, you retrieve the customer's existing loyalty points using the `listLoyaltyPoints` method, which is automatically generated by the `MedusaService`. If the customer has existing points, you update them with the new points using the `updateLoyaltyPoints` method.

Otherwise, if the customer doesn't have existing loyalty points, you create a new record with the `createLoyaltyPoints` method.

The next method you'll add deducts points from the customer's loyalty points, which is useful when the customer redeems points. Add the following method to the `LoyaltyModuleService`:

```ts title="src/modules/loyalty/service.ts"
class LoyaltyModuleService extends MedusaService({
  LoyaltyPoint,
}) {
  // ...
  async deductPoints(customerId: string, points: number): Promise<LoyaltyPoint> {
    const existingPoints = await this.listLoyaltyPoints({
      customer_id: customerId,
    })

    if (existingPoints.length === 0 || existingPoints[0].points < points) {
      throw new MedusaError(
        MedusaError.Types.NOT_ALLOWED,
        "Insufficient loyalty points"
      )
    }

    return await this.updateLoyaltyPoints({
      id: existingPoints[0].id,
      points: existingPoints[0].points - points,
    })
  }
}
```

The `deductPoints` method accepts the customer ID and the points to deduct.

In the method, you retrieve the customer's existing loyalty points using the `listLoyaltyPoints` method. If the customer doesn't have existing points or if the points to deduct are greater than the existing points, you throw an error.

Otherwise, you update the customer's loyalty points with the new value using the `updateLoyaltyPoints` method, which is automatically generated by `MedusaService`.

Next, you'll add the method that retrieves the points of a customer. Add the following method to the `LoyaltyModuleService`:

```ts title="src/modules/loyalty/service.ts"
class LoyaltyModuleService extends MedusaService({
  LoyaltyPoint,
}) {
  // ...
  async getPoints(customerId: string): Promise<number> {
    const points = await this.listLoyaltyPoints({
      customer_id: customerId,
    })

    return points[0]?.points || 0
  }
}
```

The `getPoints` method accepts the customer ID and retrieves the customer's loyalty points using the `listLoyaltyPoints` method. If the customer has no points, it returns `0`.

#### Add Method to Map Points to Discount

Finally, you'll add a method that implements the logic of mapping loyalty points to a discount amount. This is useful when the customer wants to redeem their points during checkout.

The mapping logic may differ for each use case. For example, you may need to use a third-party service to map the loyalty points discount amount, or use some custom calculation.

To simplify the logic in this tutorial, you'll use a simple calculation that maps 1 point to 1 currency unit. For example, `100` points = `$100` discount.

Add the following method to the `LoyaltyModuleService`:

```ts title="src/modules/loyalty/service.ts"
class LoyaltyModuleService extends MedusaService({
  LoyaltyPoint,
}) {
  // ...
  async calculatePointsFromAmount(amount: number): Promise<number> {
    // Convert amount to points using a standard conversion rate
    // For example, $1 = 1 point
    // Round down to nearest whole point
    const points = Math.floor(amount)

    if (points < 0) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Amount cannot be negative"
      )
    }

    return points
  }
}
```

The `calculatePointsFromAmount` method accepts the amount and converts it to the nearest whole number of points. If the amount is negative, it throws an error.

You'll use this method later to calculate the amount discounted when a customer redeems their loyalty points.

### Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/loyalty/index.ts` with the following content:

```ts title="src/modules/loyalty/index.ts"
import { Module } from "@medusajs/framework/utils"
import LoyaltyModuleService from "./service"

export const LOYALTY_MODULE = "loyalty"

export default Module(LOYALTY_MODULE, {
  service: LoyaltyModuleService,
})
```

You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `loyalty`.
2. An object with a required property `service` indicating the module's service.

You also export the module's name as `LOYALTY_MODULE` so you can reference it later.

### Add Module to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/loyalty",
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

### Generate Migrations

Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module.

Refer to the [Migrations documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md) to learn more.

Medusa's CLI tool can generate the migrations for you. To generate a migration for the Loyalty Module, run the following command in your Medusa application's directory:

```bash
npx medusa db:generate loyalty
```

The `db:generate` command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a `migrations` directory under `src/modules/loyalty` that holds the generated migration.

Then, to reflect these migrations on the database, run the following command:

```bash
npx medusa db:migrate
```

The table for the `LoyaltyPoint` data model is now created in the database.

***

## Step 3: Change Loyalty Points Flow

Now that you have a module that stores and manages loyalty points in the database, you'll start building flows around it that allow customers to earn and redeem points.

The first flow you'll build will either add points to a customer's loyalty points or deduct them based on a purchased order. If the customer hasn't redeemed points, the points are added to their loyalty points. Otherwise, the points are deducted from their loyalty points.

To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an endpoint.

In this section, you'll build the workflow that adds or deducts loyalty points for an order's customer. Later, you'll execute this workflow when an order is placed.

Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

The workflow will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the order's details.
- [validateCustomerExistsStep](#validateCustomerExistsStep): Validate that the customer is registered.
- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion.

Medusa provides the `useQueryGraphStep` and `updatePromotionsStep` in its `@medusajs/medusa/core-flows` package. So, you'll only implement the other steps.

### validateCustomerExistsStep

In the workflow, you first need to validate that the customer is registered. Only registered customers can earn and redeem loyalty points.

To do this, create the file `src/workflows/steps/validate-customer-exists.ts` with the following content:

```ts title="src/workflows/steps/validate-customer-exists.ts"
import { CustomerDTO } from "@medusajs/framework/types"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { MedusaError } from "@medusajs/framework/utils"

export type ValidateCustomerExistsStepInput = {
  customer: CustomerDTO | null | undefined
}

export const validateCustomerExistsStep = createStep(
  "validate-customer-exists",
  async ({ customer }: ValidateCustomerExistsStepInput) => {
    if (!customer) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA, 
        "Customer not found"
      )
    }

    if (!customer.has_account) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA, 
        "Customer must have an account to earn or manage points"
      )
    }
  }
)
```

You create a step with `createStep` from the Workflows SDK. It accepts two parameters:

1. The step's unique name, which is `validate-customer-exists`.
2. An async function that receives two parameters:
   - The step's input, which is in this case an object with the customer's details.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.

In the step function, you validate that the customer is defined and that it's registered based on its `has_account` property. Otherwise, you throw an error.

### getCartLoyaltyPromoStep

Next, you'll need to retrieve the loyalty promotion applied on the cart, if there's any. This is useful to determine whether the customer has redeemed points.

Before you create a step, you'll create a utility function that the step uses to retrieve the loyalty promotion of a cart. You'll create it as a separate utility function to use it later in other customizations.

Create the file `src/utils/promo.ts` with the following content:

```ts title="src/utils/promo.ts"
import { PromotionDTO, CustomerDTO, CartDTO } from "@medusajs/framework/types"

export type CartData = CartDTO & {
  promotions?: PromotionDTO[]
  customer?: CustomerDTO
  metadata: {
    loyalty_promo_id?: string
  }
}

export function getCartLoyaltyPromotion(
  cart: CartData
): PromotionDTO | undefined {
  if (!cart?.metadata?.loyalty_promo_id) {
    return
  }

  return cart.promotions?.find(
    (promotion) => promotion.id === cart.metadata.loyalty_promo_id
  )
}
```

You create a `getCartLoyaltyPromotion` function that accepts the cart's details as an input and returns the loyalty promotion if it exists. You retrieve the loyalty promotion if its ID is stored in the cart's `metadata.loyalty_promo_id` property.

You can now create the step that uses this utility to retrieve a carts loyalty points promotion. To create the step, create the file `src/workflows/steps/get-cart-loyalty-promo.ts` with the following content:

```ts title="src/workflows/steps/get-cart-loyalty-promo.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { CartData, getCartLoyaltyPromotion } from "../../utils/promo"
import { MedusaError } from "@medusajs/framework/utils"

type GetCartLoyaltyPromoStepInput = {
  cart: CartData,
  throwErrorOn?: "found" | "not-found"
}

export const getCartLoyaltyPromoStep = createStep(
  "get-cart-loyalty-promo",
  async ({ cart, throwErrorOn }: GetCartLoyaltyPromoStepInput) => {
    const loyaltyPromo = getCartLoyaltyPromotion(cart)

    if (throwErrorOn === "found" && loyaltyPromo) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Loyalty promotion already applied to cart"
      )
    } else if (throwErrorOn === "not-found" && !loyaltyPromo) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "No loyalty promotion found on cart"
      )
    }

    return new StepResponse(loyaltyPromo)
  }
)
```

You create a step that accepts an object having the following properties:

- `cart`: The cart's details.
- `throwErrorOn`: An optional property that indicates whether to throw an error if the loyalty promotion is found or not found.

The `throwErrorOn` property is useful to make the step reusable in different scenarios, allowing you to use it in later workflows.

In the step, you call the `getCartLoyaltyPromotion` utility to retrieve the loyalty promotion. If the `throwErrorOn` property is set to `found` and the loyalty promotion is found, you throw an error.

Otherwise, if the `throwErrorOn` property is set to `not-found` and the loyalty promotion is not found, you throw an error.

To return data from a step, you return an instance of `StepResponse` from the Workflows SDK. It accepts as a parameter the data to return, which is the loyalty promotion in this case.

### deductPurchasePointsStep

If the order's cart has a loyalty promotion, you need to deduct points from the customer's loyalty points. To do this, create the file `src/workflows/steps/deduct-purchase-points.ts` with the following content:

If you get a type error on resolving the Loyalty Module, run the Medusa application once with the `npm run dev` or `yarn dev` command to generate the necessary type definitions, as explained in the [Automatically Generated Types guide](https://docs.medusajs.com/docs/learn/fundamentals/generated-types/index.html.md).

```ts title="src/workflows/steps/deduct-purchase-points.ts" highlights={deductStepHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
import { 
  createStep,
  StepResponse, 
} from "@medusajs/framework/workflows-sdk"
import { LOYALTY_MODULE } from "../../modules/loyalty"
import LoyaltyModuleService from "../../modules/loyalty/service"

type DeductPurchasePointsInput = {
  customer_id: string
  amount: number
}

export const deductPurchasePointsStep = createStep(
  "deduct-purchase-points",
  async ({ 
    customer_id, amount,
  }: DeductPurchasePointsInput, { container }) => {
    const loyaltyModuleService: LoyaltyModuleService = container.resolve(
      LOYALTY_MODULE
    )

    const pointsToDeduct = await loyaltyModuleService.calculatePointsFromAmount(
      amount
    )

    const result = await loyaltyModuleService.deductPoints(
      customer_id,
      pointsToDeduct
    )

    return new StepResponse(result, {
      customer_id,
      points: pointsToDeduct,
    })
  },
  async (data, { container }) => {
    if (!data) {
      return
    }

    const loyaltyModuleService: LoyaltyModuleService = container.resolve(
      LOYALTY_MODULE
    )

    // Restore points in case of failure
    await loyaltyModuleService.addPoints(
      data.customer_id,
      data.points
    )
  }
)
```

You create a step that accepts an object having the following properties:

- `customer_id`: The ID of the customer to deduct points from.
- `amount`: The promotion's amount, which will be used to calculate the points to deduct.

In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you use the `calculatePointsFromAmount` method to calculate the points to deduct from the promotion's amount.

After that, you call the `deductPoints` method to deduct the points from the customer's loyalty points.

Finally, you return a `StepResponse` with the result of the `deductPoints`.

#### Compensation Function

This step has a compensation function, which is passed as a third parameter to the `createStep` function.

The compensation function undoes the actions performed in a step. Then, if an error occurs during the workflow's execution, the compensation functions of executed steps are called to roll back the changes. This mechanism ensures data consistency in your application, especially as you integrate external systems.

The compensation function accepts two parameters:

1. Data passed from the step function to the compensation function. The data is passed as a second parameter of the returned `StepResponse` instance.
2. An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

In the compensation function, you resolve the Loyalty Module's service from the Medusa container. Then, you call the `addPoints` method to restore the points deducted from the customer's loyalty points if an error occurs.

### addPurchaseAsPointsStep

The last step you'll create adds points to the customer's loyalty points. You'll use this step if the customer didn't redeem points during checkout.

To create the step, create the file `src/workflows/steps/add-purchase-as-points.ts` with the following content:

```ts title="src/workflows/steps/add-purchase-as-points.ts" highlights={addPointsHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { LOYALTY_MODULE } from "../../modules/loyalty"
import LoyaltyModuleService from "../../modules/loyalty/service"

type StepInput = {
  customer_id: string
  amount: number
}

export const addPurchaseAsPointsStep = createStep(
  "add-purchase-as-points",
  async (input: StepInput, { container }) => {
    const loyaltyModuleService: LoyaltyModuleService = container.resolve(
      LOYALTY_MODULE
    )

    const pointsToAdd = await loyaltyModuleService.calculatePointsFromAmount(
      input.amount
    )

    const result = await loyaltyModuleService.addPoints(
      input.customer_id,
      pointsToAdd
    )

    return new StepResponse(result, {
      customer_id: input.customer_id,
      points: pointsToAdd,
    })
  },
  async (data, { container }) => {
    if (!data) {
      return
    }

    const loyaltyModuleService: LoyaltyModuleService = container.resolve(
      LOYALTY_MODULE
    )

    await loyaltyModuleService.deductPoints(
      data.customer_id,
      data.points
    )
  }
)
```

You create a step that accepts an object having the following properties:

- `customer_id`: The ID of the customer to add points to.
- `amount`: The order's amount, which will be used to calculate the points to add.

In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you use the `calculatePointsFromAmount` method to calculate the points to add from the order's amount.

After that, you call the `addPoints` method to add the points to the customer's loyalty points.

Finally, you return a `StepResponse` with the result of the `addPoints`.

You also pass to the compensation function the customer's ID and the points added. In the compensation function, you deduct the points if an error occurs.

### Add Utility Functions

Before you create the workflow, you need a utility function that checks whether an order's cart has a loyalty promotion. This is useful to determine whether the customer redeemed points during checkout, allowing you to decide which steps to execute.

To add the utility function, add the following to `src/utils/promo.ts`:

```ts title="src/utils/promo.ts"
import { OrderDTO } from "@medusajs/framework/types"

export type OrderData = OrderDTO & {
  promotion?: PromotionDTO[]
  customer?: CustomerDTO
  cart?: CartData
}

export const CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE = "customer_id"

export function orderHasLoyaltyPromotion(order: OrderData): boolean {
  const loyaltyPromotion = getCartLoyaltyPromotion(
    order.cart as unknown as CartData
  )

  return loyaltyPromotion?.rules?.some((rule) => {
    return rule?.attribute === CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE && (
      rule?.values?.some((value) => value.value === order.customer?.id) || false
    )
  }) || false
}
```

You first define an `OrderData` type that extends the `OrderDTO` type. This type has the order's details, including the cart, customer, and promotions details.

Then, you define a constant `CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE` that represents the attribute used in the promotion rule to check whether the customer ID is valid.

Finally, you create the `orderHasLoyaltyPromotion` function that accepts an order's details and checks whether it has a loyalty promotion. It returns `true` if:

- The order's cart has a loyalty promotion. You use the `getCartLoyaltyPromotion` utility to try to retrieve the loyalty promotion.
- The promotion's rules include the `customer_id` attribute and its value matches the order's customer ID.
  - When you create the promotion for the cart later, you'll see how to set this rule.

You'll use this utility in the workflow next.

### Create the Workflow

Now that you have all the steps, you can create the workflow that uses them.

To create the workflow, create the file `src/workflows/handle-order-points.ts` with the following content:

```ts title="src/workflows/handle-order-points.ts" highlights={handleOrderPointsHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
import { createWorkflow, when } from "@medusajs/framework/workflows-sdk"
import { updatePromotionsStep, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { validateCustomerExistsStep, ValidateCustomerExistsStepInput } from "./steps/validate-customer-exists"
import { deductPurchasePointsStep } from "./steps/deduct-purchase-points"
import { addPurchaseAsPointsStep } from "./steps/add-purchase-as-points"
import { OrderData, CartData } from "../utils/promo"
import { orderHasLoyaltyPromotion } from "../utils/promo"
import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo"

type WorkflowInput = {
  order_id: string
}

export const handleOrderPointsWorkflow = createWorkflow(
  "handle-order-points",
  ({ order_id }: WorkflowInput) => {
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "id", 
        "customer.*", 
        "total", 
        "cart.*",
        "cart.promotions.*",
        "cart.promotions.rules.*",
        "cart.promotions.rules.values.*",
        "cart.promotions.application_method.*",
      ],
      filters: {
        id: order_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    validateCustomerExistsStep({
      customer: orders[0].customer,
    } as ValidateCustomerExistsStepInput)

    const loyaltyPointsPromotion = getCartLoyaltyPromoStep({
      cart: orders[0].cart as unknown as CartData,
    })

    when(orders, (orders) => 
      orderHasLoyaltyPromotion(orders[0] as unknown as OrderData) && 
      loyaltyPointsPromotion !== undefined
    )
    .then(() => {
      deductPurchasePointsStep({
        customer_id: orders[0].customer!.id,
        amount: loyaltyPointsPromotion.application_method!.value as number,
      })

      updatePromotionsStep([
        {
          id: loyaltyPointsPromotion.id,
          status: "inactive",
        },
      ])
    })


    when(
      orders, 
      (order) => !orderHasLoyaltyPromotion(order[0] as unknown as OrderData)
    )
    .then(() => {
      addPurchaseAsPointsStep({
        customer_id: orders[0].customer!.id,
        amount: orders[0].total,
      })
    })
  }
)
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is an object with the order's ID.

In the workflow's constructor function, you:

- Use `useQueryGraphStep` to retrieve the order's details. You pass the order's ID as a filter to retrieve the order.
  - This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which is a tool that retrieves data across modules.
- Validate that the customer is registered using the `validateCustomerExistsStep`.
- Retrieve the cart's loyalty promotion using the `getCartLoyaltyPromoStep`.
- Use `when` to check whether the order's cart has a loyalty promotion.
  - Since you can't perform data manipulation in a workflow's constructor function, `when` allows you to perform steps if a condition is satisfied.
  - You pass as a first parameter the object to perform the condition on, which is the order in this case. In the second parameter, you pass a function that returns a boolean value, indicating whether the condition is satisfied.
  - To specify the steps to perform if a condition is satisfied, you chain a `then` method to the `when` method. You can perform any step within the `then` method.
  - In this case, if the order's cart has a loyalty promotion, you call the `deductPurchasePointsStep` to deduct points from the customer's loyalty points. You also call the `updatePromotionsStep` to deactivate the cart's loyalty promotion.
- You use another `when` to check whether the order's cart doesn't have a loyalty promotion.
  - If the condition is satisfied, you call the `addPurchaseAsPointsStep` to add points to the customer's loyalty points.

You'll use this workflow next when an order is placed.

To learn more about the constraints on a workflow's constructor function, refer to the [Workflow Constraints](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documentation. Refer to the [When-Then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) documentation to learn more about the `when` method and how to use it in a workflow.

***

## Step 4: Handle Order Placed Event

Now that you have the workflow that handles adding or deducting loyalty points for an order, you need to execute it when an order is placed.

Medusa has an event system that allows you to listen to events emitted by the Medusa server using a [subscriber](https://docs.medusajs.com/docs//learn/fundamentals/events-and-subscribers/index.html.md). A subscriber is an asynchronous function that's executed when its associated event is emitted. In a subscriber, you can execute a workflow that performs actions in result of the event.

In this step, you'll create a subscriber that listens to the `order.placed` event and executes the `handleOrderPointsWorkflow` workflow.

Refer to the [Events and Subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) documentation to learn more.

Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, to create a subscriber, create the file `src/subscribers/order-placed.ts` with the following content:

```ts title="src/subscribers/order-placed.ts"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { handleOrderPointsWorkflow } from "../workflows/handle-order-points"

export default async function orderPlacedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await handleOrderPointsWorkflow(container).run({
    input: {
      order_id: data.id,
    },
  })
}

export const config: SubscriberConfig = {
  event: "order.placed",
}
```

The subscriber file must export:

- An asynchronous subscriber function that's executed whenever the associated event, which is `order.placed` is triggered.
- A configuration object with an event property whose value is the event the subscriber is listening to. You can also pass an array of event names to listen to multiple events in the same subscriber.

The subscriber function accepts an object with the following properties:

- `event`: An object with the event's data payload. For example, the `order.placed` event has the order's ID in its data payload.
- `container`: The Medusa container, which you can use to resolve services and tools.

In the subscriber function, you execute the `handleOrderPointsWorkflow` by invoking it, passing it the Medusa container, then using its `run` method, passing it the workflow's input.

Whenever an order is placed now, the subscriber will be executed, which in turn will execute the workflow that handles the loyalty points flow.

### Test it Out

To test out the loyalty points flow, you'll use the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) that you installed in the first step. As mentioned in that step, the storefront will be installed in a separate directory from the Medusa application, and its name is `{project-name}-storefront`, where `{project-name}` is the name of your Medusa application's directory.

So, run the following command in the Medusa application's directory to start the Medusa server:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

Then, run the following command in the Next.js Starter Storefront's directory to start the Next.js server:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

The Next.js Starter Storefront will be running on `http://localhost:8000`, and the Medusa server will be running on `http://localhost:9000`.

Open the Next.js Starter Storefront in your browser and create a new account by going to Account at the top right.

Once you're logged in, add an item to the cart and go through the checkout flow.

After you place the order, you'll see the following message in your Medusa application's terminal:

```bash
info:    Processing order.placed which has 1 subscribers
```

This message indicates that the `order.placed` event was emitted, and that your subscriber was executed.

Since you didn't redeem any points during checkout, loyalty points will be added to your account. You'll implement an API route that allows you to retrieve the loyalty points in the next step.

***

## Step 5: Retrieve Loyalty Points API Route

Next, you want to allow customers to view their loyalty points. You can show them on their profile page, or during checkout.

To expose a feature to clients, you create an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts.

You'll create an API route at the path `/store/customers/me/loyalty-points` that returns the loyalty points of the authenticated customer.

Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

So, to create an API route at the path `/store/customers/me/loyalty-points`, create the file `src/api/store/customers/me/loyalty-points/route.ts` with the following content:

```ts title="src/api/store/customers/me/loyalty-points/route.ts"

import { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { LOYALTY_MODULE } from "../../../../../modules/loyalty"
import LoyaltyModuleService from "../../../../../modules/loyalty/service"

export async function GET(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  const loyaltyModuleService: LoyaltyModuleService = req.scope.resolve(
    LOYALTY_MODULE
  )

  const points = await loyaltyModuleService.getPoints(
    req.auth_context.actor_id
  )

  res.json({
    points,
  })
}
```

Since you export a `GET` route handler function, you're exposing a `GET` endpoint at `/store/customers/me/loyalty-points`.  The route handler function accepts two parameters:

1. A request object with details and context on the request, such as body parameters or authenticated customer details.
2. A response object to manipulate and send the response.

In the route handler, you resolve the Loyalty Module's service from the Medusa container (which is available at `req.scope`).

Then, you call the service's `getPoints` method to retrieve the authenticated customer's loyalty points. Note that routes starting with `/store/customers/me` are only accessible by authenticated customers. You can access the authenticated customer ID from the request's context, which is available at `req.auth_context.actor_id`.

Finally, you return the loyalty points in the response.

You'll test out this route as you customize the Next.js Starter Storefront next.

***

## Step 6: Show Loyalty Points During Checkout

Now that you have the API route to retrieve the loyalty points, you can show them during checkout.

In this step, you'll customize the Next.js Starter Storefront to show the loyalty points in the checkout page.

First, you'll add a server action function that retrieves the loyalty points from the route you created earlier. In `src/lib/data/customer.ts`, add the following function:

```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue"
export const getLoyaltyPoints = async () => {
  const headers = {
    ...(await getAuthHeaders()),
  }

  return sdk.client.fetch<{ points: number }>(
    `/store/customers/me/loyalty-points`,
    {
      method: "GET",
      headers,
    }
  )
    .then(({ points }) => points)
    .catch(() => null)
}
```

You add a `getLoyaltyPoints` function that retrieves the authenticated customer's loyalty points from the API route you created earlier. You pass the authentication headers using the `getAuthHeaders` function, which is a utility function defined in the Next.js Starter Storefront.

If the customer isn't authenticated, the request will fail. So, you catch the error and return `null` in that case.

Next, you'll create a component that shows the loyalty points in the checkout page. Create the file `src/modules/checkout/components/loyalty-points/index.tsx` with the following content:

```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={loyaltyPointsHighlights}
"use client"

import { HttpTypes } from "@medusajs/types"
import { useEffect, useMemo, useState } from "react"
import { getLoyaltyPoints } from "../../../../lib/data/customer"
import { Button, Heading } from "@medusajs/ui"
import Link from "next/link"

type LoyaltyPointsProps = {
  cart: HttpTypes.StoreCart & {
    promotions: HttpTypes.StorePromotion[]
  }
}

const LoyaltyPoints = ({ cart }: LoyaltyPointsProps) => {
  const isLoyaltyPointsPromoApplied = useMemo(() => {
    return cart.promotions.find(
      (promo) => promo.id === cart.metadata?.loyalty_promo_id
    ) !== undefined
  }, [cart])
  const [loyaltyPoints, setLoyaltyPoints] = useState<
    number | null
  >(null)

  useEffect(() => {
    getLoyaltyPoints()
    .then((points) => {
      console.log(points)
      setLoyaltyPoints(points)
    })
  }, [])

  const handleTogglePromotion = async (
    e: React.MouseEvent<HTMLButtonElement, MouseEvent>
  ) => {
    e.preventDefault()
    // TODO apply or remove loyalty promotion
  }

  return (
    <>
      <div className="h-px w-full border-b border-gray-200 my-4" />
      <div className="flex flex-col">
        <Heading className="txt-medium mb-2">
          Loyalty Points
        </Heading>
        {loyaltyPoints === null && (
          <Link href="/account" className="txt-medium text-ui-fg-interactive hover:text-ui-fg-interactive-hover">
            Sign up to get and use loyalty points
          </Link>
        )}
        {loyaltyPoints !== null && (
          <div className="flex items-center justify-between my-6 gap-1">
          <Button
            variant="secondary"
            className="w-1/2"
            onClick={handleTogglePromotion}
          >
            {isLoyaltyPointsPromoApplied ? "Remove" : "Apply"} Loyalty Points
          </Button>
          <span className="txt-medium text-ui-fg-subtle">
            You have {loyaltyPoints} loyalty points
          </span>
        </div>
        )}
      </div>
    </>
  )
}

export default LoyaltyPoints
```

You create a `LoyaltyPoints` component that accepts the cart's details as a prop. In the component, you:

- Create a `isLoyaltyPointsPromoApplied` memoized value that checks whether the cart has a loyalty promotion applied. You use the `cart.metadata.loyalty_promo_id` property to check this.
- Create a `loyaltyPoints` state to store the customer's loyalty points.
- Call the `getLoyaltyPoints` function in a `useEffect` hook to retrieve the loyalty points from the API route you created earlier. You set the `loyaltyPoints` state with the retrieved points.
- Define `handleTogglePromotion` that, when clicked, would either apply or remove the promotion. You'll implement these functionalities later.
- Render the loyalty points in the component. If the customer isn't authenticated, you show a link to the account page to sign up. Otherwise, you show the loyalty points and a button to apply or remove the promotion.

Next, you'll show this component at the end of the checkout's summary component. So, import the component in `src/modules/checkout/templates/checkout-summary/index.tsx`:

```tsx title="src/modules/checkout/templates/checkout-summary/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import LoyaltyPoints from "../../components/loyalty-points"
```

Then, in the return statement of the `CheckoutSummary` component, add the following after the `div` wrapping the `DiscountCode`:

```tsx title="src/modules/checkout/templates/checkout-summary/index.tsx" badgeLabel="Storefront" badgeColor="blue"
<LoyaltyPoints cart={cart} />
```

This will show the loyalty points component at the end of the checkout summary.

### Test it Out

To test out the customizations to the checkout flow, make sure both the Medusa application and Next.js Starter Storefront are running.

Then, as an authenticated customer, add an item to cart and proceed to checkout. You'll find a new "Loyalty Points" section at the end of the checkout summary.

![Loyalty Points Section at the end of the summary section at the right](https://res.cloudinary.com/dza7lstvk/image/upload/v1744195223/Medusa%20Resources/Screenshot_2025-04-09_at_1.39.34_PM_l5oltc.png)

If you made a purchase before, you can see your loyalty points. You'll also see the "Apply Loyalty Points" button, which doesn't yet do anything. You'll add the functionality next.

***

## Step 7: Apply Loyalty Points to Cart

The next feature you'll implement allows the customer to apply their loyalty points during checkout. To implement the feature, you need:

- A workflow that implements the steps of the apply loyalty points flow.
- An API route that exposes the workflow's functionality to clients. You'll then send a request to this API route to apply the loyalty points on the customer's cart.
- A function in the Next.js Starter Storefront that sends the request to the API route you created earlier.

The workflow will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details.
- [validateCustomerExistsStep](#validateCustomerExistsStep): Validate that the customer is registered.
- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion.
- [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md): Acquire a lock on the cart to prevent concurrent modifications.
- [getCartLoyaltyPromoAmountStep](#getCartLoyaltyPromoAmountStep): Get the amount to be discounted based on the loyalty points.
- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md): Create a new loyalty promotion for the cart.
- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartPromotionsWorkflow/index.html.md): Update the cart's promotions with the new loyalty promotion.
- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to store the ID of the loyalty promotion in the metadata.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details again.
- [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md): Release the lock on the cart.

Most of the workflow's steps are either provided by Medusa in the `@medusajs/medusa/core-flows` package or steps you've already implemented. You only need to implement the `getCartLoyaltyPromoAmountStep` step.

### getCartLoyaltyPromoAmountStep

The fourth step in the workflow is the `getCartLoyaltyPromoAmountStep`, which retrieves the amount to be discounted based on the loyalty points. This step is useful to determine how much discount to apply to the cart.

To create the step, create the file `src/workflows/steps/get-cart-loyalty-promo-amount.ts` with the following content:

```ts title="src/workflows/steps/get-cart-loyalty-promo-amount.ts" highlights={getCartLoyaltyPromoAmountStepHighlights}
import { PromotionDTO, CustomerDTO } from "@medusajs/framework/types"
import { MedusaError } from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import LoyaltyModuleService from "../../modules/loyalty/service"
import { LOYALTY_MODULE } from "../../modules/loyalty"

export type GetCartLoyaltyPromoAmountStepInput = {
  cart: {
    id: string
    customer: CustomerDTO
    promotions?: PromotionDTO[]
    total: number
  }
}

export const getCartLoyaltyPromoAmountStep = createStep(
  "get-cart-loyalty-promo-amount",
  async ({ cart }: GetCartLoyaltyPromoAmountStepInput, { container }) => {
    // Check if customer has any loyalty points
    const loyaltyModuleService: LoyaltyModuleService = container.resolve(
      LOYALTY_MODULE
    )
    const loyaltyPoints = await loyaltyModuleService.getPoints(
      cart.customer.id
    )

    if (loyaltyPoints <= 0) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Customer has no loyalty points"
      )
    }
    
    const pointsAmount = await loyaltyModuleService.calculatePointsFromAmount(
      loyaltyPoints
    )

    const amount = Math.min(pointsAmount, cart.total)

    return new StepResponse(amount)
  }
)
```

You create a step that accepts an object having the cart's details.

In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you call the `getPoints` method to retrieve the customer's loyalty points. If the customer has no loyalty points, you throw an error.

Next, you call the `calculatePointsFromAmount` method to calculate the amount to be discounted based on the loyalty points. You use the `Math.min` function to ensure that the amount doesn't exceed the cart's total.

Finally, you return a `StepResponse` with the amount to be discounted.

### Create the Workflow

You can now create the workflow that applies a loyalty promotion to the cart.

To create the workflow, create the file `src/workflows/apply-loyalty-on-cart.ts` with the following content:

```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={applyLoyaltyOnCartWorkflowHighlights} collapsibleLines="1-26" expandButtonLabel="Show Imports"
import {
  createWorkflow,
  transform,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  acquireLockStep,
  createPromotionsStep, 
  releaseLockStep,
  updateCartPromotionsWorkflow, 
  updateCartsStep, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { 
  validateCustomerExistsStep, 
  ValidateCustomerExistsStepInput,
} from "./steps/validate-customer-exists"
import { 
  getCartLoyaltyPromoAmountStep, 
  GetCartLoyaltyPromoAmountStepInput,
} from "./steps/get-cart-loyalty-promo-amount"
import { CartData, CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE } from "../utils/promo"
import { CreatePromotionDTO } from "@medusajs/framework/types"
import { PromotionActions } from "@medusajs/framework/utils"
import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo"

type WorkflowInput = {
  cart_id: string
}

const fields = [
  "id",
  "customer.*",
  "promotions.*",
  "promotions.application_method.*",
  "promotions.rules.*",
  "promotions.rules.values.*",
  "currency_code",
  "total",
  "metadata",
]

export const applyLoyaltyOnCartWorkflow = createWorkflow(
  "apply-loyalty-on-cart",
  (input: WorkflowInput) => {
    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      fields,
      filters: {
        id: input.cart_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    validateCustomerExistsStep({
      customer: carts[0].customer,
    } as ValidateCustomerExistsStepInput)

    getCartLoyaltyPromoStep({
      cart: carts[0] as unknown as CartData,
      throwErrorOn: "found",
    })

    acquireLockStep({
      key: input.cart_id,
      timeout: 2,
      ttl: 10,
    })

    const amount = getCartLoyaltyPromoAmountStep({
      cart: carts[0],
    } as unknown as GetCartLoyaltyPromoAmountStepInput)

    // TODO create and apply the promotion on the cart
  }
)
```

You create a workflow that accepts an object with the cart's ID as input.

So far, you:

- Use `useQueryGraphStep` to retrieve the cart's details. You pass the cart's ID as a filter to retrieve the cart.
- Validate that the customer is registered using the `validateCustomerExistsStep`.
- Check whether the cart has a loyalty promotion using the `getCartLoyaltyPromoStep`. You pass the `throwErrorOn` parameter with the value `found` to throw an error if a loyalty promotion is found in the cart.
- Acquire a lock on the cart using the `acquireLockStep` to prevent concurrent modifications.
- Retrieve the amount to be discounted based on the loyalty points using the `getCartLoyaltyPromoAmountStep`.

Next, you need to create a new loyalty promotion for the cart. First, you'll prepare the data of the promotion to be created.

Replace the `TODO` with the following:

```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={prepareLoyaltyPromoDataHighlights}
const promoToCreate = transform({
  carts,
  amount,
}, (data) => {
  const randomStr = Math.random().toString(36).substring(2, 8)
  const uniqueId = (
    "LOYALTY-" + data.carts[0].customer?.first_name + "-" + randomStr
  ).toUpperCase()
  return {
    code: uniqueId,
    type: "standard",
    status: "active",
    application_method: {
      type: "fixed",
      value: data.amount,
      target_type: "order",
      currency_code: data.carts[0].currency_code,
      allocation: "across",
    },
    rules: [
      {
        attribute: CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE,
        operator: "eq",
        values: [data.carts[0].customer!.id],
      },
    ],
    campaign: {
      name: uniqueId,
      description: "Loyalty points promotion for " + data.carts[0].customer!.email,
      campaign_identifier: uniqueId,
      budget: {
        type: "usage",
        limit: 1,
      },
    },
  }
})

// TODO create promotion and apply it on cart
```

Since data manipulation isn't allowed in a workflow constructor, you use the [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) function from the Workflows SDK. It accepts two parameters:

- The data to perform manipulation on. In this case, you pass the cart's details and the amount to be discounted.
- A function that receives the data from the first parameter, and returns the transformed data.

In the transformation function, you prepare th data of the loyalty promotion to be created. Some key details include:

- You set the discount amount in the application method of the promotion.
- You add a rule to the promotion that ensures it can be used only in carts having their `customer_id` equal to this customer's ID. This prevents other customers from using this promotion.
- You create a campaign for the promotion, and you set the campaign budget to a single usage. This prevents the customer from using the promotion again.

Learn more about promotion concepts in the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md)'s documentation.

You can now use the returned data to create a promotion and apply it to the cart. Replace the new `TODO` with the following:

```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={createLoyaltyPromoStepHighlights}
const loyaltyPromo = createPromotionsStep([
  promoToCreate,
] as CreatePromotionDTO[])

const { metadata, ...updatePromoData } = transform({
  carts,
  promoToCreate,
  loyaltyPromo,
}, (data) => {
  const promos = [
    ...(data.carts[0].promotions?.map((promo) => promo?.code).filter(Boolean) || []) as string[],
    data.promoToCreate.code,
  ]

  return {
    cart_id: data.carts[0].id,
    promo_codes: promos,
    action: PromotionActions.ADD,
    metadata: {
      loyalty_promo_id: data.loyaltyPromo[0].id,
    },
  }
})

updateCartPromotionsWorkflow.runAsStep({
  input: updatePromoData,
})

updateCartsStep([
  {
    id: input.cart_id,
    metadata,
  },
])

// retrieve cart with updated promotions
const { data: updatedCarts } = useQueryGraphStep({
  entity: "cart",
  fields,
  filters: { id: input.cart_id },
}).config({ name: "retrieve-cart" })

releaseLockStep({
  key: input.cart_id,
})

return new WorkflowResponse(updatedCarts[0])
```

In the rest of the workflow, you:

- Create the loyalty promotion using the data you prepared earlier using the `createPromotionsStep`.
- Use the `transform` function to prepare the data to update the cart's promotions. You add the new loyalty promotion code to the cart's promotions codes, and set the `loyalty_promo_id` in the cart's metadata.
- Update the cart's promotions with the new loyalty promotion using the `updateCartPromotionsWorkflow` workflow.
- Update the cart's metadata with the loyalty promotion ID using the `updateCartsStep`.
- Retrieve the cart's details again using `useQueryGraphStep` to get the updated cart with the new loyalty promotion.
- Release the lock on the cart using the `releaseLockStep`.

To return data from the workflow, you must return an instance of `WorkflowResponse`. You pass it the data to be returned, which is in this case the cart's details.

### Create the API Route

Next, you'll create the API route that executes this workflow.

To create the API route, create the file `src/api/store/carts/[id]/loyalty-points/route.ts` with the following content:

```ts title="src/api/store/carts/[id]/loyalty-points/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { applyLoyaltyOnCartWorkflow } from "../../../../../workflows/apply-loyalty-on-cart"

export async function POST(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { id: cart_id } = req.params

  const { result: cart } = await applyLoyaltyOnCartWorkflow(req.scope)
    .run({
      input: {
        cart_id,
      },
    })

  res.json({ cart })
}
```

Since you export a `POST` route handler, you expose a `POST` API route at `/store/carts/[id]/loyalty-points`.

In the route handler, you execute the `applyLoyaltyOnCartWorkflow` workflow, passing it the cart ID as an input. You return the cart's details in the response.

You can now use this API route in the Next.js Starter Storefront.

### Apply Loyalty Points in the Storefront

In the Next.js Starter Storefront, you need to add a server action function that sends a request to the API route you created earlier. Then, you'll use that function when the customer clicks the "Apply Loyalty Points" button.

To add the function, add the following to `src/lib/data/cart.ts` in the Next.js Starter Storefront:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
export async function applyLoyaltyPointsOnCart() {
  const cartId = await getCartId()
  const headers = {
    ...(await getAuthHeaders()),
  }

  return await sdk.client.fetch<{
    cart: HttpTypes.StoreCart & {
      promotions: HttpTypes.StorePromotion[]
    }
  }>(`/store/carts/${cartId}/loyalty-points`, {
    method: "POST",
    headers,
  })
  .then(async (result) => {
    const cartCacheTag = await getCacheTag("carts")
    revalidateTag(cartCacheTag)

    return result
  })
}
```

You create an `applyLoyaltyPointsOnCart` function that sends a request to the API route you created earlier.

In the function, you retrieve the cart ID stored in the cookie using the `getCartId` function, which is available in the Next.js Starter Storefront.

Then, you send the request. Once the request is resolved successfully, you revalidate the cart cache tag to ensure that the cart's details are updated and refetched by other components. This ensures that the applied promotion is shown in the checkout summary without needing to refresh the page.

Finally, you'll use this function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier.

At the top of `src/modules/checkout/components/loyalty-points/index.tsx`, import the function:

```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { applyLoyaltyPointsOnCart } from "../../../../lib/data/cart"
```

Then, replace the `handleTogglePromotion` function with the following:

```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const handleTogglePromotion = async (
  e: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
  e.preventDefault()
  if (!isLoyaltyPointsPromoApplied) {
    await applyLoyaltyPointsOnCart()
  } else {
    // TODO remove loyalty points
  }
}
```

In the `handleTogglePromotion` function, you call the `applyLoyaltyPointsOnCart` function if the cart doesn't have a loyalty promotion. This will send a request to the API route you created earlier, which will execute the workflow that applies the loyalty promotion to the cart.

You'll implement removing the loyalty points promotion in a later step.

### Test it Out

To test out applying the loyalty points on the cart, start the Medusa application and Next.js Starter Storefront.

Then, in the checkout flow as an authenticated customer, click on the "Apply Loyalty Points" button. The checkout summary will be updated with the applied promotion and the discount amount.

If you don't want the promotion to be shown in the "Promotions(s) applied" section, you can filter the promotions in `src/modules/checkout/components/discount-code/index.tsx` to not show a promotion matching `cart.metadata.loyalty_promo_id`.

![Discounted amount is shown as part of the summary and the promotion is shown as part of the applied promotions](https://res.cloudinary.com/dza7lstvk/image/upload/v1744200895/Medusa%20Resources/Screenshot_2025-04-09_at_3.14.19_PM_abmtjh.png)

***

## Step 8: Remove Loyalty Points From Cart

In this step, you'll implement the functionality to remove the loyalty points promotion from the cart. This is useful if the customer changes their mind and wants to remove the promotion.

To implement this functionality, you'll need to:

- Create a workflow that removes the loyalty points promotion from the cart.
- Create an API route that executes the workflow.
- Create a function in the Next.js Starter Storefront that sends a request to the API route you created earlier.
- Use the function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier.

### Create the Workflow

The workflow will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details.
- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion.
- [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md): Acquire a lock on the cart to prevent concurrent modifications.
- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartPromotionsWorkflow/index.html.md): Update the cart's promotions to remove the loyalty promotion.
- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to remove the loyalty promotion ID from the metadata.
- [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md): Deactivate the loyalty promotion.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details again.
- [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md): Release the lock on the cart.

Since you already have all the steps, you can create the workflow.

To create the workflow, create the file `src/workflows/remove-loyalty-from-cart.ts` with the following content:

```ts title="src/workflows/remove-loyalty-from-cart.ts" collapsibleLines="1-15" expandButtonLabel="Show Imports" highlights={removeLoyaltyFromCartWorkflowHighlights}
import {
  createWorkflow,
  transform,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
  acquireLockStep,
  releaseLockStep,
  useQueryGraphStep,
  updateCartPromotionsWorkflow,
  updateCartsStep,
  updatePromotionsStep,
} from "@medusajs/medusa/core-flows"
import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo"
import { PromotionActions } from "@medusajs/framework/utils"
import { CartData } from "../utils/promo"

type WorkflowInput = {
  cart_id: string
}

const fields = [
  "id",
  "customer.*",
  "promotions.*",
  "promotions.application_method.*",
  "promotions.rules.*",
  "promotions.rules.values.*",
  "currency_code",
  "total",
  "metadata",
]

export const removeLoyaltyFromCartWorkflow = createWorkflow(
  "remove-loyalty-from-cart",
  (input: WorkflowInput) => {
    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      fields,
      filters: {
        id: input.cart_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const loyaltyPromo = getCartLoyaltyPromoStep({
      cart: carts[0] as unknown as CartData,
      throwErrorOn: "not-found",
    })

    acquireLockStep({
      key: input.cart_id,
      timeout: 2,
      ttl: 10,
    })

    updateCartPromotionsWorkflow.runAsStep({
      input: {
        cart_id: input.cart_id,
        promo_codes: [loyaltyPromo.code!],
        action: PromotionActions.REMOVE,
      },
    })

    const newMetadata = transform({
      carts,
    }, (data) => {
      const { loyalty_promo_id, ...rest } = data.carts[0].metadata || {}

      return {
        ...rest,
        loyalty_promo_id: null,
      }
    })

    updateCartsStep([
      {
        id: input.cart_id,
        metadata: newMetadata,
      },
    ])

    updatePromotionsStep([
      {
        id: loyaltyPromo.id,
        status: "inactive",
      },
    ])

    // retrieve cart with updated promotions
    const { data: updatedCarts } = useQueryGraphStep({
      entity: "cart",
      fields,
      filters: { id: input.cart_id },
    }).config({ name: "retrieve-cart" })

    releaseLockStep({
      key: input.cart_id,
    })

    return new WorkflowResponse(updatedCarts[0])
  }
)
```

You create a workflow that accepts an object with the cart's ID as input.

In the workflow, you:

- Use `useQueryGraphStep` to retrieve the cart's details. You pass the cart's ID as a filter to retrieve the cart.
- Check whether the cart has a loyalty promotion using the `getCartLoyaltyPromoStep`. You pass the `throwErrorOn` parameter with the value `not-found` to throw an error if a loyalty promotion isn't found in the cart.
- Acquire a lock on the cart using the `acquireLockStep` to prevent concurrent modifications.
- Update the cart's promotions using the `updateCartPromotionsWorkflow`, removing the loyalty promotion.
- Use the `transform` function to prepare the new metadata of the cart. You remove the `loyalty_promo_id` from the metadata.
- Update the cart's metadata with the new metadata using the `updateCartsStep`.
- Deactivate the loyalty promotion using the `updatePromotionsStep`.
- Retrieve the cart's details again using `useQueryGraphStep` to get the updated cart with the new loyalty promotion.
- Release the lock on the cart using the `releaseLockStep`.
- Return the cart's details in a `WorkflowResponse` instance.

### Create the API Route

Next, you'll create the API route that executes this workflow.

To create the API route, add the following in `src/api/store/carts/[id]/loyalty-points/route.ts`:

```ts title="src/api/store/carts/[id]/loyalty-points/route.ts"
// other imports...
import { removeLoyaltyFromCartWorkflow } from "../../../../../workflows/remove-loyalty-from-cart"

// ...
export async function DELETE(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { id: cart_id } = req.params

  const { result: cart } = await removeLoyaltyFromCartWorkflow(req.scope)
    .run({
      input: {
        cart_id,
      },
    })

  res.json({ cart })
}
```

You export a `DELETE` route handler, which exposes a `DELETE` API route at `/store/carts/[id]/loyalty-points`.

In the route handler, you execute the `removeLoyaltyFromCartWorkflow` workflow, passing it the cart ID as an input. You return the cart's details in the response.

You can now use this API route in the Next.js Starter Storefront.

### Remove Loyalty Points in the Storefront

In the Next.js Starter Storefront, you need to add a server action function that sends a request to the API route you created earlier. Then, you'll use that function when the customer clicks the "Remove Loyalty Points" button, which shows when the cart has a loyalty promotion applied.

To add the function, add the following to `src/lib/data/cart.ts`:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
export async function removeLoyaltyPointsOnCart() {
  const cartId = await getCartId()
  const headers = {
    ...(await getAuthHeaders()),
  }
  const next = {
    ...(await getCacheOptions("carts")),
  }

  return await sdk.client.fetch<{
    cart: HttpTypes.StoreCart & {
      promotions: HttpTypes.StorePromotion[]
    }
  }>(`/store/carts/${cartId}/loyalty-points`, {
    method: "DELETE",
    headers,
  })
  .then(async (result) => {
    const cartCacheTag = await getCacheTag("carts")
    revalidateTag(cartCacheTag)

    return result
  })
}
```

You create a `removeLoyaltyPointsOnCart` function that sends a request to the API route you created earlier.

In the function, you retrieve the cart ID stored in the cookie using the `getCartId` function, which is available in the Next.js Starter Storefront.

Then, you send the request to the API route. Once the request is resolved successfully, you revalidate the cart cache tag to ensure that the cart's details are updated and refetched by other components. This ensures that the promotion is removed from the checkout summary without needing to refresh the page.

Finally, you'll use this function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier.

At the top of `src/modules/checkout/components/loyalty-points/index.tsx`, add the following import:

```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { removeLoyaltyPointsOnCart } from "../../../../lib/data/cart"
```

Then, replace the `TODO` in `handleTogglePromotion` with the following:

```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue"
await removeLoyaltyPointsOnCart()
```

In the `handleTogglePromotion` function, you call the `removeLoyaltyPointsOnCart` function if the cart has a loyalty promotion. This will send a request to the API route you created earlier, which will execute the workflow that removes the loyalty promotion from the cart.

### Test it Out

To test out removing the loyalty points from the cart, start the Medusa application and Next.js Starter Storefront.

Then, in the checkout flow as an authenticated customer, after applying the loyalty points, click on the "Remove Loyalty Points" button. The checkout summary will be updated with the removed promotion and the discount amount.

![The "Remove Loyalty Points" button is shown in the "Loyalty Points" section](https://res.cloudinary.com/dza7lstvk/image/upload/v1744204436/Medusa%20Resources/Screenshot_2025-04-09_at_4.13.24_PM_xt5trh.png)

***

## Step 9: Validate Loyalty Points on Cart Completion

After the customer applies the loyalty points to the cart and places the order, you need to validate that the customer actually has the loyalty points. This prevents edge cases where the customer may have applied the loyalty points previously but they don't have them anymore.

So, in this step, you'll hook into Medusa's cart completion flow to perform the validation.

Since Medusa uses workflows in its API routes, it allows you to hook into them and perform custom functionalities using [Workflow Hooks](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md). A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler.

Medusa uses the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) hook to complete the cart and place an order. This workflow has a `validate` hook that allows you to perform custom validation before the cart is completed.

To consume the `validate` hook, create the file `src/workflows/hooks/complete-cart.ts` with the following content:

```ts title="src/workflows/hooks/complete-cart.ts" highlights={completeCartWorkflowHookHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { completeCartWorkflow } from "@medusajs/medusa/core-flows"
import LoyaltyModuleService from "../../modules/loyalty/service"
import { LOYALTY_MODULE } from "../../modules/loyalty"
import { CartData, getCartLoyaltyPromotion } from "../../utils/promo"
import { MedusaError } from "@medusajs/framework/utils"

completeCartWorkflow.hooks.validate(
  async ({ cart }, { container }) => {
    const query = container.resolve("query")
    const loyaltyModuleService: LoyaltyModuleService = container.resolve(
      LOYALTY_MODULE
    )

    const { data: carts } = await query.graph({
      entity: "cart",
      fields: [
        "id", 
        "promotions.*", 
        "customer.*", 
        "promotions.rules.*", 
        "promotions.rules.values.*", 
        "promotions.application_method.*", 
        "metadata",
      ],
      filters: {
        id: cart.id,
      },
    }, {
      throwIfKeyNotFound: true,
    })

    const loyaltyPromo = getCartLoyaltyPromotion(
      carts[0] as unknown as CartData
    )

    if (!loyaltyPromo) {
      return
    }
    
    const customerLoyaltyPoints = await loyaltyModuleService.getPoints(
      carts[0].customer!.id
    )
    const requiredPoints = await loyaltyModuleService.calculatePointsFromAmount(
      loyaltyPromo.application_method!.value as number
    )

    if (customerLoyaltyPoints < requiredPoints) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        `Customer does not have enough loyalty points. Required: ${
          requiredPoints
        }, Available: ${customerLoyaltyPoints}`
      )
    }
  }
)
```

Workflows have a special `hooks` property that includes all the hooks that you can consume in that workflow. You consume the hook by invoking it from the workflow's `hooks` property.

Since the hook is essentially a step function, it accepts the following parameters:

- The hook's input passed from the workflow, which differs for each hook. The `validate` hook receives an object having the cart's details.
- The step context object, which contains the Medusa container. You can use it to resolve services and perform actions.

In the hook, you resolve Query and the Loyalty Module's service. Then, you use Query to retrieve the cart's necessary details, including its promotions, customer, and metadata.

After that, you retrieve the customer's loyalty points and calculate the required points to apply the loyalty promotion.

If the customer doesn't have enough loyalty points, you throw an error. This will prevent the cart from being completed if the customer doesn't have enough loyalty points.

***

## Test Out Cart Completion with Loyalty Points

Since you now have the entire loyalty points flow implemented, you can test it out by going through the checkout flow, applying the loyalty points to the cart.

When you place the order, if the customer has sufficient loyalty points, the validation hook will pass.

Then, the `order.placed` event will be emitted, which will execute the subscriber that calls the `handleOrderPointsWorkflow`.

In the workflow, since the order's cart has a loyalty promotion, the points equivalent to the promotion will be deducted, and the promotion becomes inactive.

You can confirm that the loyalty points were deducted either by sending a request to the [retrieve loyalty points API route](#step-5-retrieve-loyalty-points-api-route), or by going through the checkout process again in the storefront.

***

## Next Steps

You've now implement a loyalty points system in Medusa. There's still more that you can implement based on your use case:

- Add loyalty points on registration or other events. Refer to the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for a full list of available events you can listen to.
- Show the customer their loyalty point usage history. This will require adding another data model in the Loyalty Module that records the usage history. You can create records of that data model when an order that has a loyalty promotion is placed, then customize the storefront to show a new page for loyalty points history.
- Customize the Medusa Admin to show a new page or [UI Route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) for loyalty points information and analytics.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Implement Phone Authentication and Integrate Twilio SMS

In this tutorial, you will learn how to implement phone number authentication in your Medusa application.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md), which are available out-of-the-box. These features include authentication with custom providers and for custom user or actor types.

In this tutorial, you'll learn how to implement a custom authentication provider that allows customers to log in with their phone number. You'll also integrate [Twilio](https://www.twilio.com/en-us/messaging/channels/sms) to send SMS messages to those customers with the one-time password (OTP) for authentication.

Twilio is just one option to deliver the OTP to the customer. You can integrate a different SMS provider or use a different method to send OTPs.

## Summary

By following this tutorial, you will learn how to:

- Install and set up Medusa.
- Implement a custom phone authentication provider.
- Integrate Twilio to send OTPs by SMS.
- Customize the Next.js Starter Storefront to allow customers to log in with their phone numbers.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Diagram showcasing the phone authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1747744941/Medusa%20Resources/phone-auth-overview_bt5yrp.jpg)

While this tutorial focuses on supporting phone authentication for customers, you can use the authentication provider for any actor type, such as admin user or vendor. [At the end of this tutorial](#next-steps), you'll learn how to authenticate other actor types.

- [Phone Authentication Repository](https://github.com/medusajs/examples/tree/main/phone-auth): Find the full code for this guide in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1747745832/OpenApi/Phone_Auth_g4xsqv.yaml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.

Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Implement Phone Authentication Module Provider

In Medusa, you integrate custom authentication providers by creating an [Authentication Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/index.html.md). Then, you can use that provider to authenticate users using custom logic.

In this step, you'll create a Phone Authentication Module Provider that allows users to log in with their phone numbers and an OTP. Later, you'll integrate Twilio to send the OTPs to the users, and customize the storefront to allow customers to log in with their phone numbers.

An Authentication Module Provider doesn't need to handle storing and managing specific user details, such as creating customers or admin users. Instead, it only focuses on the logic of authenticating a type of user using custom logic or integration. You can learn more in the [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md) documentation.

### Prerequisite: Install jsonwebtoken

In the Phone Authentication Module Provider, you'll use the `jsonwebtoken` package to sign and verify the OTPs.

To install the package, run the following command in the Medusa application directory:

```bash npm2yarn
npm install jsonwebtoken
npm install @types/jsonwebtoken --save-dev
```

### a. Create Module Directory

Modules are created under the `src/modules` directory. So, start by creating the directory `src/modules/phone-auth`.

### b. Create Auth Module Provider Service

A module has a service that contains its logic. For Authentication Module Providers, the service implements the logic to authenticate users.

To create the service of the Phone Authentication Module Provider, create the file `src/modules/phone-auth/service.ts` with the following content:

```ts title="src/modules/phone-auth/service.ts" highlights={phoneAuthServiceHighlights}
import { 
  AbstractAuthModuleProvider, 
  AbstractEventBusModuleService, 
} from "@medusajs/framework/utils"
import { 
  Logger, 
} from "@medusajs/types"

type InjectedDependencies = {
  logger: Logger
  event_bus: AbstractEventBusModuleService
}

type Options = {
  jwtSecret: string
}

class PhoneAuthService extends AbstractAuthModuleProvider {
  static DISPLAY_NAME = "Phone Auth"
  static identifier = "phone-auth"
  private options: Options
  private logger: Logger
  private event_bus: AbstractEventBusModuleService

  constructor(container: InjectedDependencies, options: Options) {
    // @ts-ignore
    super(...arguments)

    this.options = options
    this.logger = container.logger
    this.event_bus = container.event_bus
  }
}

export default PhoneAuthService
```

An Authentication Module Provider's service must extend the `AbstractAuthModuleProvider` class. You'll get a type error about implementing the abstract methods of that class, which you'll add in the next steps.

An Authentication Module Provider must also have the following static properties:

- `identifier`: A unique identifier for the provider.
- `DISPLAY_NAME`: A human-readable name for the provider. This name is used for display purposes.

A module provider's constructor receives two parameters:

- `container`: The [module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) that contains Framework resources available to the module. You access the following resources:
  - `logger`: A [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) class to log debug messages.
  - `event_bus`: The [Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/index.html.md)'s service to emit events.
- `options`: Options that are passed to the module provider when it's registered in Medusa's configurations. You define the following option:
  - `jwtSecret`: A secret used to sign and verify the OTPs.

You'll learn how to set this option when you [add the module provider to Medusa's configurations](#h-add-module-provider-to-medusas-configurations).

In the constructor, you set the class's properties to the injected dependencies and options.

In the next sections, you'll implement the methods of the `AbstractAuthModuleProvider` class.

Refer to the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider/index.html.md) guide for detailed information about the methods.

### c. Implement validateOptions Method

The `validateOptions` method is used to validate the options passed to the module provider. If the method throws an error, the Medusa application won't start.

So, add the `validateOptions` method to the `PhoneAuthService` class:

```ts title="src/modules/phone-auth/service.ts"
// other imports...
import { 
  MedusaError,
} from "@medusajs/framework/utils"

class PhoneAuthService extends AbstractAuthModuleProvider {
  // ...
  static validateOptions(options: Record<any, any>): void | never {
    if (!options.jwtSecret) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "JWT secret is required"
      )
    }
  }
}
```

The `validateOptions` method receives the options passed to the module provider as a parameter.

In the method, you throw an error if the `jwtSecret` option is not set.

### d. Implement register Method

When a customer (or another actor type) registers in your application, they must also have an [auth identity](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-identity-and-actor-types/index.html.md) that allows them to login.

The `register` method of an auth provider uses custom logic to create the auth identity for the actor type (such as customer). In the method, you can perform custom validation and specify the custom authentication details to store for the user's auth identity.

Medusa uses the `register` method to create an auth identity that will be associated with the customer when they register. You can learn more in the [Authentication Flows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-flows/index.html.md) documentation.

![Diagram showcasing the relation between a customer and auth identity](https://res.cloudinary.com/dza7lstvk/image/upload/v1747747179/Medusa%20Resources/customer-auth-identity_je1bvh.jpg)

So, add the `register` method to the `PhoneAuthService` class:

```ts title="src/modules/phone-auth/service.ts" highlights={registerHighlights}
// other imports...
import { 
  AuthenticationInput, 
  AuthIdentityProviderService, 
  AuthenticationResponse, 
} from "@medusajs/types"

class PhoneAuthService extends AbstractAuthModuleProvider {
  // ...
  async register(
    data: AuthenticationInput,
    authIdentityProviderService: AuthIdentityProviderService
  ): Promise<AuthenticationResponse> {
    const { phone } = data.body || {}

    if (!phone) {
      return {
        success: false,
        error: "Phone number is required",
      }
    }

    try {
      await authIdentityProviderService.retrieve({
        entity_id: phone,
      })

      return {
        success: false,
        error: "User with phone number already exists",
      }
    } catch (error) {
      const user = await authIdentityProviderService.create({
        entity_id: phone,
      })

      return {
        success: true,
        authIdentity: user,
      }
    }
  }
}
```

#### Parameters

The `register` method receives an object parameter with the following properties:

- `data`: An object containing properties like `body` that holds request-body parameters. Clients will pass relevant authentication data, such as the user's phone number, in the request body.
- `authIdentityProviderService`: A service injected by the [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md) that allows you to manage auth identities.

The method receives other parameters, which you can find in the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider#register/index.html.md) guide.

#### Method Logic

In the method, you extract the `phone` property from the request body, and return an error if it's not provided. You also return an error if another user is using the same phone number.

Otherwise, you create a new auth identity for the user. You set the phone number as the `entity_id` of the auth identity, which is a unique identifier.

#### Return Value

Finally, you return an object with the following properties:

- `success`: A boolean indicating whether the registration was successful.
- `authIdentity`: The created auth identity of the user. This property is only set if the registration was successful.
- `error`: An error message if the registration failed.

### e. Implement authenticate Method

When a customer (or another actor type) logs in, the `authenticate` method of an auth provider is called. This method uses custom logic to authenticate the user.

Authentication providers may implement one of the following flows:

- Direct authentication, where the user is authenticated using this method only. For example, authenticating with an email and password.
- Authentication with callback verification, where the user is authenticated using this method and then a callback is used to verify additional information.

For the Phone Authentication Module Provider, you'll implement the second flow. The user will first be authenticated using the `authenticate` method to make sure the user exists and generate an OTP. Then, they need to supply the OTP to verify their identity.

![Diagram showcasing authentication with callback verification](https://res.cloudinary.com/dza7lstvk/image/upload/v1747749683/Medusa%20Resources/authentication-flow_ey59hw.jpg)

So, add the `authenticate` method to the `PhoneAuthService` class:

```ts title="src/modules/phone-auth/service.ts" highlights={authenticateHighlights}
// other imports...
import { 
  AuthIdentityDTO,
} from "@medusajs/types"
import jwt from "jsonwebtoken"

class PhoneAuthService extends AbstractAuthModuleProvider {
  // ...
  async authenticate(
    data: AuthenticationInput,
    authIdentityProviderService: AuthIdentityProviderService
  ): Promise<AuthenticationResponse> {
    const { phone } = data.body || {}

    if (!phone) {
      return {
        success: false,
        error: "Phone number is required",
      }
    }

    try {
      await authIdentityProviderService.retrieve({
        entity_id: phone,
      })
    } catch (error) {
      return {
        success: false,
        error: "User with phone number does not exist",
      }
    }

    const { hashedOTP, otp } = await this.generateOTP()

    await authIdentityProviderService.update(phone, {
      provider_metadata: {
        otp: hashedOTP,
      },
    })

    await this.event_bus.emit({
      name: "phone-auth.otp.generated",
      data: {
        otp,
        phone,
      },
    }, {})

    return {
      success: true,
      location: "otp",
    }
  }

  async generateOTP(): Promise<{ hashedOTP: string, otp: string }> {
    // Generate a 6-digit OTP
    const otp = Math.floor(100000 + Math.random() * 900000).toString()

    // for debug
    this.logger.info(`Generated OTP: ${otp}`)
    
    const hashedOTP = jwt.sign({ otp }, this.options.jwtSecret, {
      expiresIn: "60s",
    })
    
    return { hashedOTP, otp }
  }
}
```

You add two methods: the `authenticate` method, and a helper `generateOTP` method.

#### authenticate Parameters

The `authenticate` method receives an object parameter with the following properties:

- `data`: An object containing properties like `body` that holds request-body parameters. Clients will pass relevant authentication data, such as the user's phone number, in the request body.
- `authIdentityProviderService`: A service injected by the [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md) that allows you to manage auth identities.

The method receives other parameters, which you can find in the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider#authenticate/index.html.md) guide.

#### authenticate Logic

In the method, you return an error if the `phone` property is not provided in the request body, or if a user with that phone number doesn't exist.

Next, you generate a 6-digit OTP using the `generateOTP` method. Notice that you currently log the OTP for debugging purposes. You can remove this line later once you integrate Twilio.

The OTP is hashed and stored in the `provider_metadata` property of the user's auth identity. The `provider_metadata` property is a JSON object that stores additional information about the auth identity.

Then, you emit an event with the generated OTP and the user's phone number. This allows you later to handle the event and send the OTP to the user using services like Twilio.

#### authenticate Return Value

Finally, you return an object with the following properties:

- `success`: A boolean indicating whether the authentication was successful.
- `location`: A string indicating a URL to perform additional actions in. In this case, you set the location to `otp`, indicating that the user should verify with the OTP.
- `error`: An error message if the authentication failed.

### f. Implement validateCallback Method

When an authentication provider requires a callback to verify the user, the Medusa application calls the `validateCallback` method.

You can use this method to verify the OTP that the user entered. If valid, you return the logged in user, and the Medusa application will return a JWT token that the user can use to authenticate in the application.

![Diagram showcasing how callback verification fits in the authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1747749734/Medusa%20Resources/callback-flow_yney5a.jpg)

So, add the `validateCallback` method to the `PhoneAuthService` class:

```ts title="src/modules/phone-auth/service.ts" highlights={validateCallbackHighlights}
class PhoneAuthService extends AbstractAuthModuleProvider {
  // ...
  async validateCallback(
    data: AuthenticationInput,
    authIdentityProviderService: AuthIdentityProviderService
  ): Promise<AuthenticationResponse> {
    const { phone, otp } = data.query || {}

    if (!phone || !otp) {
      return {
        success: false,
        error: "Phone number and OTP are required",
      }
    }

    const user = await authIdentityProviderService.retrieve({
      entity_id: phone,
    })

    if (!user) {
      return {
        success: false,
        error: "User with phone number does not exist",
      }
    }
    
    // verify that OTP is correct
    const userProvider = user.provider_identities?.find((provider) => provider.provider === this.identifier)
    if (!userProvider || !userProvider.provider_metadata?.otp) {
      return {
        success: false,
        error: "User with phone number does not have a phone auth provider",
      }
    }
    
    try {
      const decodedOTP = jwt.verify(
        userProvider.provider_metadata.otp as string, 
        this.options.jwtSecret
      ) as { otp: string }
  
      if (decodedOTP.otp !== otp) {
        throw new Error("Invalid OTP")
      }
    } catch (error) {
      return {
        success: false,
        error: error.message || "Invalid OTP",
      }
    }
    
    const updatedUser = await authIdentityProviderService.update(phone, {
      provider_metadata: {
        otp: null,
      },
    })

    return {
      success: true,
      authIdentity: updatedUser,
    }
  }
}
```

#### Parameters

The `validateCallback` method receives an object parameter with the following properties:

- `data`: An object containing properties like `query` that holds query parameters. Clients will pass relevant authentication data, such as the user's phone number and OTP, in the request query.
- `authIdentityProviderService`: A service injected by the [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md) that allows you to manage auth identities.

The method receives other parameters, which you can find in the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider#validatecallback/index.html.md) guide.

#### Method Logic

In the method, you return an error if the phone and otp aren't provided in the request query, or if a user with that phone number doesn't exist.

Next, you verify that the OTP provided by the user is correct. You retrieve the hashed OTP from the `provider_metadata` property of the user's auth identity. If the OTP is not valid, you return an error.

Since you set the hash expiration to 60 seconds, the OTP will be valid for 60 seconds. After that, the user will need to request a new OTP.

After that, you update the user's auth identity to remove the OTP from the `provider_metadata` property.

#### Return Value

Finally, you return an object with the following properties:

- `success`: A boolean indicating whether the authentication was successful.
- `authIdentity`: The user's auth identity. This property is only set if the authentication was successful.
- `error`: An error message if the authentication failed.

### g. Export Module Definition

You've now finished implementing the necessary methods for the Phone Authentication Module Provider.

The final piece to a module is its definition, which you export in an `index.ts` file at the module's root directory. This definition tells Medusa the name of the module, its service, and optionally its loaders.

To create the module's definition, create the file `src/modules/phone-auth/index.ts` with the following content:

```ts title="src/modules/phone-auth/index.ts"
import PhoneAuthService from "./service"
import { 
  ModuleProvider, 
  Modules,
} from "@medusajs/framework/utils"

export default ModuleProvider(Modules.AUTH, {
  services: [PhoneAuthService],
})
```

You use `ModuleProvider` from the Modules SDK to create the module provider's definition. It accepts two parameters:

1. The name of the module that this provider belongs to, which is `Modules.AUTH` in this case.
2. An object with a required property `services` indicating the module provider's services. Each of these services will be registered as authentication providers in Medusa.

### h. Add Module Provider to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property:

```ts title="medusa-config.ts"
// other imports...
import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils"

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/auth",
      dependencies: [
        Modules.CACHE, 
        ContainerRegistrationKeys.LOGGER, 
        Modules.EVENT_BUS,
      ],
      options: {
        providers: [
          // default provider
          {
            resolve: "@medusajs/medusa/auth-emailpass",
            id: "emailpass",
          },
          {
            resolve: "./src/modules/phone-auth",
            id: "phone-auth",
            options: {
              jwtSecret: process.env.PHONE_AUTH_JWT_SECRET || "supersecret",
            },
          },
        ],
      },
    },
  ],
})
```

To pass an Auth Module Provider to the Auth Module, you add the `modules` property to the Medusa configuration and pass the Auth Module in its value.

The Auth Module accepts a `dependencies` option, allowing you to inject dependencies into the containers of the module and its providers. The Auth Module requires passing the [Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/index.html.md) and Logger, but you also inject the `event_bus` dependency to use the Event Module's service in the Phone Authentication Module Provider.

The Auth Module also accepts a `providers` option, which is an array of Auth Module Providers to register. You register the `emailpass` provider, which is registered by default when you don't provide any other providers.

To register the Phone Authentication Module Provider, you add an object to the `providers` array with the following properties:

- `resolve`: The NPM package or path to the module provider. In this case, it's the path to the `src/modules/phone-auth` directory.
- `id`: The ID of the module provider. The auth provider is then registered with the ID `au_{id}`.
- `options`: The options to pass to the module provider. These are the options you defined in the `Options` interface of the module provider's service.

### i. Enable Phone Authentication for Customers

By default, customers and admin users can be authenticated using the `emailpass` provider. When you add a new provider, you need to specify which actor types can use it.

In `medusa-config.ts`, add to `projectConfig.http` a new `authMethodsPerActor` property:

```ts title="medusa-config.ts" highlights={authMethodsPerActorHighlights}
module.exports = defineConfig({
  projectConfig: {
    // ...
    http: {
      // ...
      authMethodsPerActor: {
        user: ["emailpass"],
        customer: ["emailpass", "phone-auth"],
      },
    },
  },
  // ...
})
```

The `authMethodsPerActor` property is an object whose keys are actor types. The values are arrays of authentication method IDs that can be used for that actor type.

In this case, you enable the `phone-auth` provider for customers. You can also enable it for other actor types, such as admin users or vendors.

### Test Out Phone Authentication

In this section, you'll test out the Phone Authentication Module Provider using Medusa's API routes. You can, instead, test it out later using the [Next.js Starter Storefront](#step-4-use-phone-authentication-in-the-nextjs-starter-storefront).

First, start the Medusa application with the following command:

```bash npm2yarn
npm run start
```

#### Prerequisite: Retrieve Publishable API Key

Before you start testing the authentication provider using the API routes, you need to retrieve your application's publishable API key. This key is necessary to send requests to API routes starting with `/store`.

To retrieve the publishable API key:

1. Open the Medusa Admin dashboard at `http://localhost:9000/admin` and log in.
2. Go to Settings -> Publishable API Keys.
3. Click on the API key in the table.
4. In its details page, click on the API key to copy it.

![Publishable API Key page with the API key clicked](https://res.cloudinary.com/dza7lstvk/image/upload/fl_lossy/f_auto/r_16/ar_16:9,c_pad/v1/User%20Guide/Screenshot_2025-02-25_at_6.14.15_PM_muwq9e.png)

#### a. Retrieve Registration Token

The first step is to retrieve a registration token for a new customer. This token will allow them to register in the application.

To retrieve the registration token, send a `POST` request to `/auth/customer/phone-auth/register`:

```bash
curl -X POST 'http://localhost:9000/auth/customer/phone-auth/register' \
--header 'Content-Type: application/json' \
--data '{
    "phone": "+19077890116"
}'
```

Make sure to replace the phone number with the one you want to use.

This will return a `token` in the response:

```json title="Example Response"
{
  "token": "123..."
}
```

#### b. Register Customer

Next, you'll register the customer using the [Register Customer](https://docs.medusajs.com/api/store#customers_postcustomers) API route. You'll pass the registration token you received in the previous step in the header of this request.

So, send a `POST` request to `/store/customers`:

```bash
curl -X POST 'http://localhost:9000/store/customers' \
--header 'x-publishable-api-key: {publishable_api_key}' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer {reg_token}' \
--data-raw '{
    "email": "+19077890116@gmail.com",
    "phone": "19077890116",
    "first_name": "John",
    "last_name": "Smith"
}'
```

Make sure to replace:

- `{publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin dashboard.
- `{reg_token}` with the registration token you received in the previous step.
- The customer details in the request body with the ones you want to use. Use the same phone number you used in the previous step.
  - You pass the email because it's required by the [Register Customer](https://docs.medusajs.com/api/store#customers_postcustomers) API route. You set it to the phone number with a `gmail.com` domain.

The request will return the created customer's details:

```json title="Example Response"
{
  "customer": {
    "id": "cus_01JVPESW5SM1MSVPNM2MSC0ZEC",
    "email": "+19077890116@gmail.com",
    "company_name": null,
    "first_name": "John",
    "last_name": "Smith",
    "phone": "19077890116",
    "metadata": null,
    "has_account": true,
    "deleted_at": null,
    "created_at": "2025-05-20T09:01:13.273Z",
    "updated_at": "2025-05-20T09:01:13.273Z",
    "addresses": []
  }
}
```

The customer can now authenticate using the phone number and OTP.

#### c. Authenticate Customer

Next, you'll authenticate the customer using the [Authenticate Customer](https://docs.medusajs.com/api/store#auth_postactor_typeauth_provider) API route. This would send the customer an OTP to their phone number (which you'll implement in the next step).

So, send a `POST` request to `/auth/customer/phone-auth`:

```bash
curl -X POST 'http://localhost:9000/auth/customer/phone-auth' \
--header 'Content-Type: application/json' \
--data '{
    "phone": "+19077890116"
}'
```

Make sure to replace the phone number with the one you used to register the customer.

This will return a `location` in the response:

```json title="Example Response"
{
  "location": "otp"
}
```

Indicating that the user should verify their OTP.

You can also use this route to resend the OTP if the user didn't receive it or if a minute has passed since the last OTP was sent.

#### d. Verify OTP

If you check the logs of the Medusa application, you'll see that the OTP was generated and logged:

```bash
info:    Generated OTP: 576794
```

As mentioned before, this is only for debugging purposes. In the next step, you'll implement the logic to send the OTP to the user using Twilio.

So, to verify the OTP, you'll send a request to the [Verify Callback API route](https://docs.medusajs.com/api/store#auth_postactor_typeauth_providercallback):

```bash
curl -X POST 'http://localhost:9000/auth/customer/phone-auth/callback?phone=%2B19077890116&otp=476588'
```

You pass the following query parameters:

- `phone`: The phone number of the customer. Make sure to use the same phone number you used to register the customer, and to encode it. For example, the `+` sign should be encoded as `%2B`.
- `otp`: The OTP that the customer received. Make sure to use the same OTP shown in the logs.

If the OTP is valid, you'll receive a JWT token in the response:

```json title="Example Response"
{
  "token": "123..."
}
```

You can use this token to authenticate the customer in the application. For example, you can use the token to [retrieve the customer's details](https://docs.medusajs.com/api/store#customers_getcustomersme).

If the OTP has expired, send a request to the [Authenticate Customer](#c-authenticate-customer) API route to generate a new OTP

***

## Step 3: Integrate Twilio SMS

Similar to the Auth Module, the [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) allows registering custom providers to send notifications, such as SMS or email.

In this step, you'll create a Twilio Notification Module Provider, then use it to send the OTP to the customer.

### Prerequisites

- [Twilio Account](https://console.twilio.com/)
- [Twilio From Phone Number](https://www.twilio.com/docs/phone-numbers)
- [Twilio Account SID, which you can retrieve from the Twilio Console homepage.](https://www.twilio.com/docs/usage/tutorials/how-to-use-your-free-trial-account-namer#console-dashboard-home-page)
- [Twilio Auth Token, which you can retrieve from the Twilio Console homepage.](https://www.twilio.com/docs/usage/tutorials/how-to-use-your-free-trial-account-namer#console-dashboard-home-page)

### a. Install Twilio SDK

Before you start implementing the Twilio Notification Module Provider, install the Twilio SDK to interact with the Twilio API.

Run the following command in the Medusa application directory:

```bash npm2yarn
npm install twilio
```

You'll use the Twilio SDK in the Notification Module Provider's service.

### b. Create Module Directory

Create the directory `src/modules/twilio-sms` to create the Twilio Notification Module Provider.

### c. Create Notification Module Provider Service

A Notification Module Provider has a service that contains the sending logic. The service must extend the `AbstractNotificationProviderService` class.

So, create the file `src/modules/twilio-sms/service.ts` with the following content:

```ts title="src/modules/twilio-sms/service.ts" highlights={twilioSmsServiceHighlights}
import { 
  AbstractNotificationProviderService,
} from "@medusajs/framework/utils"
import { Twilio } from "twilio"

type InjectedDependencies = {}

type TwilioSmsServiceOptions = {
  accountSid: string
  authToken: string
  from: string
}

class TwilioSmsService extends AbstractNotificationProviderService {
  static readonly identifier = "twilio-sms"
  private readonly client: Twilio
  private readonly from: string

  constructor(container: InjectedDependencies, options: TwilioSmsServiceOptions) {
    super()

    this.client = new Twilio(options.accountSid, options.authToken)
    this.from = options.from
  }
}
```

You'll get a type error about implementing the abstract methods of the `AbstractNotificationProviderService` class, which you'll add in the next steps.

A Notification Module Provider must have an `identifier` static property, which is a unique identifier for the module. This identifier is used to register the module in the Medusa application.

A module provider's constructor receives two parameters:

- `container`: The [module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) that contains Framework resources available to the module. You don't need to access any resources for this provider.
- `options`: Options that are passed to the module provider when it's registered in Medusa's configurations. You define the following option:
  - `accountSid`: The Twilio account SID.
  - `authToken`: The Twilio auth token.
  - `from`: The Twilio phone number to send the SMS from.

You'll learn how to set these options when you [add the module provider to Medusa's configurations](#g-add-module-provider-to-medusas-configurations).

In the constructor, you set the class's properties to the injected dependencies and options.

In the next sections, you'll implement the methods of the `AbstractNotificationProviderService` class.

Refer to the [Create Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md) guide for detailed information about the methods.

### d. Implement validateOptions Method

The `validateOptions` method is used to validate the options passed to the module provider. If the method throws an error, the Medusa application won't start.

So, add the `validateOptions` method to the `TwilioSmsService` class:

```ts title="src/modules/twilio-sms/service.ts"
class TwilioSmsService extends AbstractNotificationProviderService {
  // ...
  static validateOptions(options: Record<any, any>): void | never {
    if (!options.accountSid) {
      throw new Error("Account SID is required")
    }
    if (!options.authToken) {
      throw new Error("Auth token is required")
    }
    if (!options.from) {
      throw new Error("From is required")
    }
  }
}
```

The `validateOptions` method receives the options passed to the module provider as a parameter.

In the method, you throw an error if any of the options are not set.

### e. Implement send Method

The only required method for a Notification Module Provider is the `send` method. When the Medusa application needs to send a notification using the provider's channel (such as SMS), it calls this method of the registered provider.

So, add the `send` method to the `TwilioSmsService` class:

```ts title="src/modules/twilio-sms/service.ts" highlights={sendHighlights}
// other imports...
import { 
  ProviderSendNotificationDTO, 
  ProviderSendNotificationResultsDTO,
} from "@medusajs/types"

class TwilioSmsService extends AbstractNotificationProviderService {
  // ...
  async send(
    notification: ProviderSendNotificationDTO
  ): Promise<ProviderSendNotificationResultsDTO> {
    const { to, content, template, data } = notification
    const contentText = content?.text || await this.getTemplateContent(
      template, data
    )

    const message = await this.client.messages.create({
      body: contentText,
      from: this.from,
      to,
    })

    return {
      id: message.sid,
    }
  }

  async getTemplateContent(
    template: string, 
    data?: Record<string, unknown> | null
  ): Promise<string> {
    switch (template) {
      case "otp-template":
        if (!data?.otp) {
          throw new Error("OTP is required for OTP template")
        }

        return `Your OTP is ${data.otp}`
      default:
        throw new Error(`Template ${template} not found`)
    }
  }
}
```

You implement the `send` method and a helper `getTemplateContent` method.

#### send Parameters

The `send` method receives an object parameter with the following properties:

- `to`: The phone number to send the SMS to.
- `content`: An object containing the content of the SMS. The `text` property is the text to send.
- `template`: The template to use for the SMS. This is used to retrieve the fallback content of the SMS if `content.text` is not provided.
- `data`: An object containing the data to use in the template. This is used to replace placeholders in the template with actual values.

The method receives other parameters, which you can find in the [Create Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module#send/index.html.md) guide.

#### send Method Logic

In the method, you set the SMS content either to the `text` property of the `content` object or to the template content. You define a `getTemplateContent` method that retrieves the content for a template.

Then, you use the `messages.create` method of the Twilio client to send the SMS. You pass the following parameters:

- `body`: The content of the SMS.
- `from`: The Twilio phone number to send the SMS from.
- `to`: The phone number to send the SMS to.

#### send Return Value

Finally, you return an object that has an `id` property with the ID of the sent SMS. This ID is stored in the notification record in the database.

### f. Export Module Definition

You've now finished implementing the necessary methods for the Twilio Notification Module Provider. You only need to export its definition.

To create the module's definition, create the file `src/modules/twilio-sms/index.ts` with the following content:

```ts title="src/modules/twilio-sms/index.ts"
import { 
  ModuleProvider, 
  Modules,
} from "@medusajs/framework/utils"
import TwilioSMSNotificationService from "./service"

export default ModuleProvider(Modules.NOTIFICATION, {
  services: [TwilioSMSNotificationService],
})
```

You use `ModuleProvider` from the Modules SDK to create the module provider's definition passing it two parameters:

1. The name of the module that this provider belongs to, which is `Modules.NOTIFICATION` in this case.
2. An object with a required property `services` indicating the module provider's services. Each of these services will be registered as notification providers in Medusa.

### g. Add Module Provider to Medusa's Configurations

You'll now add the Twilio Notification Module Provider to Medusa's configurations to start using it.

In `medusa-config.ts`, add the following to the `modules` property:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    // ...
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          // default provider
          {
            resolve: "@medusajs/medusa/notification-local",
            id: "local",
            options: {
              name: "Local Notification Provider",
              channels: ["feed"],
            },
          },
          {
            resolve: "./src/modules/twilio-sms",
            id: "twilio-sms",
            options: {
              channels: ["sms"],
              accountSid: process.env.TWILIO_ACCOUNT_SID,
              authToken: process.env.TWILIO_AUTH_TOKEN,
              from: process.env.TWILIO_FROM,
            },
          },
        ],
      },
    },
  ],
})
```

You pass the Notification Module in the `modules` property to register the Twilio Notification Module Provider.

The Notification Module accepts a `providers` option, which is an array of Notification Module Providers to register. You register the `local` provider, which is registered by default when you don't provide any other providers.

To register the Twilio Notification Module Provider, you add an object with the following properties:

- `resolve`: The path to the module provider.
- `id`: The ID of the module provider. The notification provider is then registered with the ID `np_{identifier}_{id}`.
- `options`: The options to pass to the module provider. These include the options you defined in the `Options` interface of the module provider's service.
  - `channels`: The channels that the notification provider supports. In this case, you set it to `sms`, which is the channel used to send SMS notifications.
  - `accountSid`: The Twilio account SID.
  - `authToken`: The Twilio auth token.
  - `from`: The Twilio phone number to send the SMS from.

### h. Add Environment Variables

To set the value of the Twilio options, add the following environment variables to your `.env` file:

```shell title=".env"
TWILIO_ACCOUNT_SID=AC...
TWILIO_AUTH_TOKEN=05...
TWILIO_FROM=+1...
```

Where:

- `TWILIO_ACCOUNT_SID`: The Twilio account SID.
- `TWILIO_AUTH_TOKEN`: The Twilio auth token.
- `TWILIO_FROM`: The Twilio phone number to send the SMS from. Make sure to use the phone number you purchased from Twilio.

You can retrieve these information from the Twilio Console homepage.

![Twilio console homepage showing the account SID, phone number, and auth token](https://res.cloudinary.com/dza7lstvk/image/upload/v1747736641/Medusa%20Resources/CleanShot_2025-05-20_at_13.22.55_2x_sztmfo.png)

### i. Handle OTP Generated Event

Now that you have integrated Twilio into Medusa, you can use it to send the OTP to the customer. To do that, you need to handle the `phone-auth.otp.generated` event that you emitted in the `authenticate` method of the Phone Authentication Module Provider.

You can listen to events in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). A subscriber is an asynchronous function that listens to events to perform actions when the event is emitted.

In this step, you'll create a subscriber that listens to the `phone-auth.otp.generated` event and sends an SMS to the customer with the OTP.

Refer to the [Events and Subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) documentation to learn more.

Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, to create a subscriber, create the file `src/subscribers/send-otp.ts` with the following content:

```ts title="src/subscribers/send-otp.ts" highlights={sendOtpHighlights}
import {
  SubscriberArgs,
  type SubscriberConfig,
} from "@medusajs/medusa"
import { Modules } from "@medusajs/framework/utils"

export default async function sendOtpHandler({
  event: { data: {
    phone,
    otp,
  } },
  container,
}: SubscriberArgs<{ phone: string, otp: string }>) {
  const notificationModuleService = container.resolve(
    Modules.NOTIFICATION
  )

  await notificationModuleService.createNotifications({
    to: phone,
    channel: "sms",
    template: "otp-template",
    data: {
      otp,
    },
  })
}

export const config: SubscriberConfig = {
  event: "phone-auth.otp.generated",
}
```

The subscriber file must export:

- An asynchronous subscriber function that's executed whenever the associated event is triggered.
- A configuration object with an event property whose value is the event the subscriber is listening to, which is `phone-auth.otp.generated`.

The subscriber function accepts an object with the following properties:

- `event`: An object with the event's data payload. In the `authenticate` method, you emitted the event with the following data:
  - `phone`: The phone number of the user.
  - `otp`: The OTP that was generated.
- `container`: The [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which you can use to resolve Framework and commerce resources.

In the subscriber function, you resolve the Notification Module's service from the Medusa container. Then, you use its `createNotifications` method to send the OTP to the user.

Under the hood, the Notification Module's service delegates the sending to the Notification Module Provider of the `sms` channel, which is the Twilio Notification Module Provider in this case.

The `createNotifications` method accepts an object with the following properties:

- `to`: The phone number to send the notification to. You use the phone number from the event's data payload.
- `channel`: The channel to use to send the notification, which is `sms`.
- `template`: The template to use for the notification content, which is `otp-template`.
- `data`: An object containing the data to use in the template. You pass the OTP to the template.

### j. Test it Out

To test out the Twilio Notification Module Provider, you can follow the steps in the [Test Out Phone Authentication](#test-out-phone-authentication) section.

After you authenticate the customer, the OTP will be sent to the customer's phone number using Twilio. Then, you can use the OTP to verify the authentication and receive a JWT token.

Alternatively, you can also test it out after customizing the Next.js Starter Storefront, which you'll do in the next step.

Make sure to remove the OTP logging line in the `generateOTP` method of the Phone Authentication Module Provider's service now that you have integrated Twilio.

***

## Step 4: Use Phone Authentication in the Next.js Starter Storefront

In this step, you'll customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to allow customers to authenticate using their phone number and OTP.

By default, the Next.js Starter Storefront supports email and password authentication. You'll replace it with phone authentication, but you can also keep both authentication methods if you want to.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-phone-auth`, you can find the storefront by going back to the parent directory and changing to the `medusa-phone-auth-storefront` directory:

```bash
cd ../medusa-phone-auth-storefront # change based on your project name
```

### a. Install Phone Input Package

To easily show a phone input where the user can enter their phone number, install the `react-phone-number-input` package:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm install react-phone-number-input
```

You'll use it in the login and registration forms to show a phone input.

### b. Add Authenticate Function

Before adding the forms, you'll add the functions that send requests to the Medusa API to authenticate the customer.

The first one you'll add is the `authenticateWithPhone` function, which sends a request to the `/auth/customer/phone-auth` API route to authenticate the customer using their phone number.

In `src/lib/data/customer.ts`, add the following function:

```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue"
export const authenticateWithPhone = async (phone: string) => {
  try {
    const response = await sdk.auth.login("customer", "phone-auth", {
      phone,
    })

    if (
      typeof response === "string" || 
      !response.location || 
      response.location !== "otp"
    ) {
      throw new Error("Failed to login")
    }

    return true
  } catch (error: any) {
    return error.toString()
  }
}
```

The function accepts the phone number as a parameter.

In the function, you use the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), which is configured within the Next.js Starter Storefront, to send a request to the `/auth/customer/phone-auth` API route. You pass the phone number in the request body.

If the request doesn't return a `location` property set to `otp`, you throw an error. Otherwise, you return `true` to indicate that the request was successful.

### c. add Verify OTP Function

Next, you'll add the `verifyOTP` function, which sends a request to the `/auth/customer/phone-auth/callback` API route to verify the OTP.

In `src/lib/data/customer.ts`, add the following function:

```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue" highlights={verifyOtpHighlights}
export const verifyOtp = async ({
  otp,
  phone,
}: {
  otp: string
  phone: string
}) => {
  try {
    const token = await sdk.auth.callback("customer", "phone-auth", {
      phone,
      otp,
    })

    await setAuthToken(token)

    const customerCacheTag = await getCacheTag("customers")
    revalidateTag(customerCacheTag)

    await transferCart()

    return true
  } catch (e: any) {
    return e.toString()
  }  
}
```

The function accepts an object with the following properties:

- `otp`: The OTP to verify.
- `phone`: The phone number of the customer.

In the function, you use the JS SDK to send a request to the `/auth/customer/phone-auth/callback` API route. You pass the phone number and OTP in the request body.

If the request is successful and you receive a token, you set the token in the cookies and refresh the customer cache. This ensures that all customer-related UI is updated after logging in, such as showing the customer's profile when accessing the `/account` page.

Then, you call the `transferCart` function to transfer the cart from the guest user to the authenticated customer.

Finally, you return `true` to indicate that the request was successful.

### d. Add Registration Function

The last function you'll add is the `registerWithPhone` function, which will register the customer using their phone number.

In `src/lib/data/customer.ts`, add the following function:

```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue" highlights={registerWithPhoneHighlights}
export const registerWithPhone = async ({
  firstName,
  lastName,
  phone,
}: {
  firstName: string
  lastName: string
  phone: string
}) => {
  try {
    const { token: regToken } = await sdk.client.fetch<
      { token: string }
    >(`/auth/customer/phone-auth/register`, {
      method: "POST",
      body: {
        phone,
      },
    })
    
    await setAuthToken(regToken as string)
    const headers = {
      ...(await getAuthHeaders()),
    }

    const email = `${phone}@gmail.com`
    const customerData = {
      email,
      first_name: firstName,
      last_name: lastName,
      phone,
    }
    
    await sdk.store.customer.create(
      customerData,
      {},
      headers
    )

    return await authenticateWithPhone(phone)
  } catch (error: any) {
    return error.toString()
  }
}
```

The function accepts an object with the following properties:

- `firstName`: The first name of the customer.
- `lastName`: The last name of the customer.
- `phone`: The phone number of the customer.

In the function, you retrieve a registration token for the customer using the `/auth/customer/phone-auth/register` API route. You pass the phone number in the request body.

Then, after setting the registration token in the cookies, you create a customer using the [Create Customer](https://docs.medusajs.com/api/store#customers_postcustomers) API route. You pass the following properties in the request body:

- `email`: The email of the customer. You set it to the phone number with a `@gmail.com` domain.
- `first_name`: The first name of the customer.
- `last_name`: The last name of the customer.
- `phone`: The phone number of the customer.

Finally, you call the `authenticateWithPhone` function to authenticate the customer using their phone number. At this step, the customer would receive an OTP to login.

### e. Add OTP Form

Next, you'll add an OTP form that allows the customer to enter the OTP they receive after login or registration. Later, you'll reuse this form in both the login and registration pages.

Create the file `src/modules/account/components/otp/index.tsx` with the following content:

```tsx title="src/modules/account/components/otp/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={otpHighlights}
"use client"

import { Input } from "@medusajs/ui"
import { useState, useRef, useEffect } from "react"
import { authenticateWithPhone, verifyOtp } from "../../../../lib/data/customer"
import ErrorMessage from "../../../checkout/components/error-message"

type Props = {
  phone: string
}

export const Otp = ({ phone }: Props) => {
  const [otp, setOtp] = useState<string>("")
  const [error, setError] = useState<string>("")
  const [isLoading, setIsLoading] = useState<boolean>(false)
  const [countdown, setCountdown] = useState<number>(60)
  const inputRefs = useRef<(HTMLInputElement | null)[]>([])

  const handleSubmit = async () => {
    setIsLoading(true)
    const response = await verifyOtp({
      otp,
      phone,
    })
    setOtp("")
    setIsLoading(false)

    if (typeof response === "string") {
      setError(response)
    }
  }

  const handleResend = async () => {
    authenticateWithPhone(phone)
    setCountdown(60)
  }

  const handlePaste = (e: React.ClipboardEvent) => {
    e.preventDefault()
    const pastedData = e.clipboardData.getData("text")
    const numericValue = pastedData.replace(/\D/g, "").slice(0, 6)
    
    if (numericValue) {
      setOtp(numericValue)
      // Focus the next empty input after pasted content
      const nextEmptyIndex = Math.min(numericValue.length, 5)
      inputRefs.current[nextEmptyIndex]?.focus()
    }
  }

  // TODO add use effects
}
```

You create an `Otp` component that accepts the phone number as a prop.

In the component, you define the following state variables:

- `otp`: The OTP entered by the customer.
- `error`: The error message to show if the OTP verification fails.
- `isLoading`: A boolean indicating whether the OTP verification is in progress.
- `countdown`: The countdown timer for resending the OTP.
- `inputRefs`: A ref to store the input elements for the OTP digits. You'll show six input elements for the OTP digits.

You also define the following functions:

- `handleSubmit`: This function is called when the customer submits the OTP. It calls the `verifyOtp` function to verify the OTP entered by the customer.
- `handleResend`: This function is called when the customer clicks the "Resend OTP" button that you'll add later. It calls the `authenticateWithPhone` function to resend the OTP to the customer's phone number.
- `handlePaste`: This function is called when the customer pastes the OTP in the input field. It improves the experience of pasting the OTP without having to enter it manually.

#### Handle Variable Changes

Next, you'll add `useEffect` hooks to handle changes in the state variables.

Replace the `TODO` with the following:

```tsx title="src/modules/account/components/otp/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={useEffectHighlights}
useEffect(() => {
  if (inputRefs.current[0]) {
    inputRefs.current[0].focus()
  }
}, [inputRefs.current])

useEffect(() => {
  if (otp.length !== 6 || isLoading) {
    return
  }

  handleSubmit()
}, [otp, isLoading])

useEffect(() => {
  const timer = setInterval(() => {
    setCountdown((prev) => {
      return prev > 0 ? prev - 1 : 0
    })
  }, 1000)

  return () => clearInterval(timer)
}, [])

// TODO render form
```

You add three `useEffect` hooks:

1. The first one focuses the first input element when the component mounts.
2. The second one automatically submits the OTP when the customer enters six digits.
3. The third one adds an interval to update the countdown timer every second.

### f. Render OTP Form

Lastly, you'll render the OTP form with the input elements and the resend button.

Replace the `TODO` with the following:

```tsx title="src/modules/account/components/otp/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div
    className="max-w-sm flex flex-col items-center"
    data-testid="otp-page"
  >
    <h1 className="text-large-semi uppercase mb-6">
      Verify Phone Number
    </h1>
    <p className="text-center text-base-regular text-ui-fg-base mb-4">
      Enter the code sent to your phone number to login.
    </p>
    <div className="flex gap-2 mb-4">
      {[...Array(6)].map((_, index) => (
        <Input
          key={index}
          type="text"
          maxLength={1}
          pattern="\d*"
          inputMode="numeric"
          disabled={isLoading}
          className="w-10 h-10 text-center"
          ref={(el) => {
            inputRefs.current[index] = el
          }}
          onPaste={handlePaste}
          value={otp[index] || ""}
          onChange={(e) => {
            const elm = e.target
            const value = elm.value
            setOtp((prev) => {
              const newOtp = prev.split("")
              newOtp[index] = value
              return newOtp.join("")
            })
            if (value && /^\d+$/.test(value)) {
              // Move focus to next input
              const nextInput = elm.parentElement?.nextElementSibling?.querySelector("input")
              nextInput?.focus()
            }
          }}
          onKeyDown={(e) => {
            if (e.key === "Backspace" && !e.currentTarget.value) {
              // Move focus to previous input on backspace
              const prevInput = e.currentTarget.parentElement?.previousElementSibling?.querySelector("input")
              prevInput?.focus()
            }
          }}
        />
      ))}        
    </div>
    <div className="flex items-center gap-x-2 mb-4">
      <button
        className="text-small-regular text-ui-fg-interactive disabled:text-ui-fg-disabled disabled:cursor-not-allowed"
        onClick={handleResend}
        disabled={countdown > 0}
      >
        {countdown > 0 ? `Resend code in ${countdown}s` : "Resend Code"}
      </button>
    </div>
    <ErrorMessage error={error} />
  </div>
)
```

You show six input elements for the OTP digits. When a value is entered in an input, the focus moves to the next input. In addition, when the customer presses backspace on an empty input, the focus moves to the previous input.

You also show a resend button that allows the customer to resend the OTP once the countdown timer reaches zero.

You now have an OTP form that you can use in both the login and registration pages.

### g. Add Registration Form

You'll now add a registration form that allows the customer to register using their phone number.

First, in `src/modules/account/templates/login-template.tsx`, update the `LOGIN_VIEW` to the following:

```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"], ["5"]]}
export enum LOGIN_VIEW {
  SIGN_IN = "sign-in",
  REGISTER = "register",
  REGISTER_PHONE = "register-phone",
  SIGN_IN_PHONE = "sign-in-phone",
}
```

By default, the login template supports switching between the login and registration views for email and password authentication. With the above change, you add two new views: login and registration with phone number.

Then, to add the registration form, create the file `src/modules/account/components/register-phone/index.tsx` with the following content:

```tsx title="src/modules/account/components/register-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={registerPhoneHighlights}
"use client"

import { useState } from "react"
import Input from "@modules/common/components/input"
import { LOGIN_VIEW } from "@modules/account/templates/login-template"
import ErrorMessage from "@modules/checkout/components/error-message"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import { registerWithPhone } from "@lib/data/customer"
import "react-phone-number-input/style.css"
import PhoneInput from "react-phone-number-input"
import { Otp } from "../otp"
import { useParams } from "next/navigation"
import { Button } from "@medusajs/ui"

type Props = {
  setCurrentView: (view: LOGIN_VIEW) => void
}

const RegisterPhone = ({ setCurrentView }: Props) => {
  const [firstName, setFirstName] = useState("")
  const [lastName, setLastName] = useState("")
  const [phone, setPhone] = useState("")
  const [error, setError] = useState("")
  const [loading, setLoading] = useState(false)
  const [enterOtp, setEnterOtp] = useState(false)
  const { countryCode } = useParams() as { countryCode: string }

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    setLoading(true)
    const response = await registerWithPhone({
      firstName,
      lastName,
      phone,
    })
    setLoading(false)
    if (typeof response === "string") {
      setError(response)
      return
    }

    setEnterOtp(true)
  }

  if (enterOtp) {
    return <Otp phone={phone} />
  }

  // TODO render form
}

export default RegisterPhone
```

You create a `RegisterPhone` component that accepts a `setCurrentView` prop to switch between the login and registration views.

In the component, you define the following state variables:

- `firstName`, `lastName`, and `phone` to store the form inputs' values.
- `error`: The error message to show if the registration fails.
- `loading`: A boolean indicating whether the registration is in progress.
- `enterOtp`: A boolean indicating whether to show the OTP form. This is enabled once the customer is registered and they need to authenticate using the OTP.
- `countryCode`: The country code of the customer, which is retrieved from the URL parameters. You'll use this to show the phone input with a default selected country.

You also define a `handleSubmit` function that handles the form submission. It calls the `registerWithPhone` function to register the customer using their phone number.

If the registration is successful, you set `enterOtp` to `true` to show the OTP form. Otherwise, you set the error message.

#### Render Registration Form

Next, you'll render the registration form with the input fields and the submit button.

Replace the `TODO` with the following:

```tsx title="src/modules/account/components/register-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div
    className="max-w-sm flex flex-col items-center"
    data-testid="register-page"
  >
    <h1 className="text-large-semi uppercase mb-6">
      Become a Medusa Store Member
    </h1>
    <p className="text-center text-base-regular text-ui-fg-base mb-4">
      Create your Medusa Store Member profile, and get access to an enhanced
      shopping experience.
    </p>
    <form className="w-full flex flex-col" onSubmit={handleSubmit}>
      <div className="flex flex-col w-full gap-y-2">
        <Input
          label="First name"
          name="first_name"
          required
          autoComplete="given-name"
          data-testid="first-name-input"
          value={firstName}
          onChange={(e) => setFirstName(e.target.value)}
        />
        <Input
          label="Last name"
          name="last_name"
          required
          autoComplete="family-name"
          data-testid="last-name-input"
          value={lastName}
          onChange={(e) => setLastName(e.target.value)}
        />
        <PhoneInput
          placeholder="Enter phone number"
          value={phone}
          onChange={(value) => setPhone(value as string)}
          name="phone"
          required
          autoComplete="off"
          // @ts-ignore
          defaultCountry={countryCode.toUpperCase()}
        />
      </div>
      <ErrorMessage error={error} data-testid="register-error" />
      <span className="text-center text-ui-fg-base text-small-regular mt-6">
        By creating an account, you agree to Medusa Store&apos;s{" "}
        <LocalizedClientLink
          href="/content/privacy-policy"
          className="underline"
        >
          Privacy Policy
        </LocalizedClientLink>{" "}
        and{" "}
        <LocalizedClientLink
          href="/content/terms-of-use"
          className="underline"
        >
          Terms of Use
        </LocalizedClientLink>
        .
      </span>
      <Button 
        className="w-full mt-6" 
        type="submit"
        size="large"
        variant="primary"
        isLoading={loading}
      >
        Join
      </Button>
    </form>
    <span className="text-center text-ui-fg-base text-small-regular mt-6">
      Already a member?{" "}
      <button
        onClick={() => setCurrentView(LOGIN_VIEW.SIGN_IN_PHONE)}
        className="underline"
      >
        Sign in
      </button>
      .
    </span>
  </div>
)
```

You render the registration form with input fields for the first name, last name, and phone number.

For the phone number input, you use the `PhoneInput` component from the `react-phone-number-input` package. You set the `defaultCountry` prop to the country code retrieved from the URL parameters.

You also show a submit button that calls the `handleSubmit` function when clicked, and a button to switch to the login form.

#### Add to Login Template

Next, you'll add the `RegisterPhone` component to the login template.

In `src/modules/account/templates/login-template.tsx`, add the following import:

```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue"
import RegisterPhone from "@modules/account/components/register-phone"
```

Then, change the `return` statement of the `LoginTemplate` component to the following:

```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div className="w-full flex justify-start px-8 py-8">
    {currentView === "sign-in" ? (
      <Login setCurrentView={setCurrentView} />
    ) : currentView === "register" ? (
      <Register setCurrentView={setCurrentView} />
    ) : currentView === "register-phone" ? (
      <RegisterPhone setCurrentView={setCurrentView} />
    ) : (
      // TODO: Add login phone view
      <></>
    )}
  </div>
)
```

You show the registration form when the `currentView` is set to `register-phone`. You'll also add the login form later.

### h. Add Login Form

Next, you'll add a login form that allows the customer to log in using their phone number.

To create the form, create the file `src/modules/account/components/login-phone/index.tsx` with the following content:

```tsx title="src/modules/account/components/login-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={loginPhoneHighlights}
"use client"

import { authenticateWithPhone } from "@lib/data/customer"
import { LOGIN_VIEW } from "@modules/account/templates/login-template"
import ErrorMessage from "@modules/checkout/components/error-message"
import { useState } from "react"
import "react-phone-number-input/style.css"
import PhoneInput from "react-phone-number-input"
import { Otp } from "../otp"
import { useParams } from "next/navigation"
import { Button } from "@medusajs/ui"

type Props = {
  setCurrentView: (view: LOGIN_VIEW) => void
}

const LoginPhone = ({ setCurrentView }: Props) => {
  const [phone, setPhone] = useState("")
  const [error, setError] = useState("")
  const [loading, setLoading] = useState(false)
  const [enterOtp, setEnterOtp] = useState(false)
  const { countryCode } = useParams() as { countryCode: string }

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    setLoading(true)
    const response = await authenticateWithPhone(phone)
    setLoading(false)
    if (typeof response === "string") {
      setError(response)
      return
    }

    setEnterOtp(true)
  }

  if (enterOtp) {
    return <Otp phone={phone} />
  }

  // TODO render form
}

export default LoginPhone
```

You create a `LoginPhone` component that accepts a `setCurrentView` prop to switch between the login and registration views.

In the component, you define the following state variables:

- `phone`: The phone number entered by the customer.
- `error`: The error message to show if the login fails.
- `loading`: A boolean indicating whether the login is in progress.
- `enterOtp`: A boolean indicating whether to show the OTP form. This is enabled after the form is submitted.
- `countryCode`: The country code of the customer, which is retrieved from the URL parameters. You'll use this to show the phone input with a default selected country.

You also define a `handleSubmit` function that handles the form submission. It calls the `authenticateWithPhone` function to authenticate the customer using their phone number. Then, it enables `enterOtp` to show the OTP form.

#### Render Login Form

Next, you'll render the login form with the input field and the submit button.

Replace the `TODO` with the following:

```tsx title="src/modules/account/components/login-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div
    className="max-w-sm w-full flex flex-col items-center"
    data-testid="login-page"
  >
    <h1 className="text-large-semi uppercase mb-6">Welcome back</h1>
    <p className="text-center text-base-regular text-ui-fg-base mb-8">
      Sign in to access an enhanced shopping experience.
    </p>
    <form className="w-full" onSubmit={handleSubmit}>
      <div className="flex flex-col w-full gap-y-2">
        <PhoneInput
          placeholder="Enter phone number"
          value={phone}
          onChange={(value) => setPhone(value as string)}
          name="phone"
          required
          // @ts-ignore
          defaultCountry={countryCode.toUpperCase()}
        />
      </div>
      {error && <ErrorMessage error={error} data-testid="login-error-message" />}
      <Button 
        className="w-full mt-6" 
        disabled={loading}
        type="submit"
        size="large"
        variant="primary"
        isLoading={loading}
      >
        Sign in
      </Button>
    </form>
    <span className="text-center text-ui-fg-base text-small-regular mt-6">
      Not a member?{" "}
      <button
        onClick={() => setCurrentView(LOGIN_VIEW.REGISTER_PHONE)}
        className="underline"
        data-testid="register-button"
      >
        Join us
      </button>
      .
    </span>
  </div>
)
```

You render the login form with an input field for the phone number. You also show a submit button that calls the `handleSubmit` function when clicked.

### i. Add to Login Template

Next, you'll add the `LoginPhone` component to the login template.

In `src/modules/account/templates/login-template.tsx`, add the following import:

```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue"
import LoginPhone from "../components/login-phone"
```

Next, in the `LoginTemplate` component, change the default value of the `currentView` state to `LOGIN_VIEW.SIGN_IN_PHONE`:

```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue"
const [currentView, setCurrentView] = useState(LOGIN_VIEW.SIGN_IN_PHONE)
```

This ensures the phone login form is shown by default.

Finally, replace the `return` statement of the `LoginTemplate` component to the following:

```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div className="w-full flex justify-start px-8 py-8">
    {currentView === "sign-in" ? (
      <Login setCurrentView={setCurrentView} />
    ) : currentView === "register" ? (
      <Register setCurrentView={setCurrentView} />
    ) : currentView === "register-phone" ? (
      <RegisterPhone setCurrentView={setCurrentView} />
    ) : (
      <LoginPhone setCurrentView={setCurrentView} />
    )}
  </div>
)
```

You show the login form when the `currentView` is set to `sign-in-phone`.

### Test it Out

You can now test out the phone authentication feature in the Next.js Starter Storefront.

First, start the Medusa application by running the following command in the Medusa project's directory:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

Then, start the Next.js Starter Storefront by running the following command in the storefront project's directory:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

Open your browser, navigate to `http://localhost:8000`, and click on the "Account" link at the top right. This will show the login form with just the phone number input.

![Login form with phone number input](https://res.cloudinary.com/dza7lstvk/image/upload/v1747739945/Medusa%20Resources/CleanShot_2025-05-20_at_14.18.36_2x_ubuika.png)

You can also switch to the registration form by clicking on the "Join" link below the login form.

![Registration form with phone number input](https://res.cloudinary.com/dza7lstvk/image/upload/v1747739988/Medusa%20Resources/CleanShot_2025-05-20_at_14.19.35_2x_btlc2s.png)

You can try to login with the account you created before, or register with a new one. Once successful, you'll see the OTP form to enter the OTP you received as SMS.

![OTP form](https://res.cloudinary.com/dza7lstvk/image/upload/v1747740102/Medusa%20Resources/CleanShot_2025-05-20_at_14.21.18_2x_yrkuyg.png)

After you enter the six digits, you'll be logged in and you'll see your profile page.

![Profile page](https://res.cloudinary.com/dza7lstvk/image/upload/v1747740166/Medusa%20Resources/CleanShot_2025-05-20_at_14.22.30_2x_kshb83.png)

***

## Step 5: Disallow Phone Updates

The phone authentication feature is now complete, but there are two improvements you can make:

1. Show the phone number in the profile page: Currently, it shows the email address, which is a fake address you've set.
2. Disable phone and email updates: Currently, the customer can update their phone number, which is not allowed for phone authentication.

### a. Show Phone Number in Profile Page

To show the phone number in the profile page instead of the email, in `src/modules/account/components/overview/index.tsx`, find the following in the `return` statement:

```tsx title="src/modules/account/components/overview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
<span
  className="font-semibold"
  data-testid="customer-email"
  data-value={customer?.email}
>
  {customer?.email}
</span>
```

And replace it with the following:

```tsx title="src/modules/account/components/overview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
<span
  className="font-semibold"
  data-testid="customer-phone"
  data-value={customer?.phone}
>
  {customer?.phone}
</span>
```

If you check the profile page now, you'll see the phone number instead of the email address at the top right.

![Phone number showing in profile page](https://res.cloudinary.com/dza7lstvk/image/upload/v1747740378/Medusa%20Resources/CleanShot_2025-05-20_at_14.25.59_2x_x0pxvt.png)

### b. Remove Email and Phone Fields

Next, you'll remove the fields to update email and phone number from the profile page.

In `src/app/[countryCode]/(main)/account/@dashboard/profile/page.tsx`, find the following lines to remove from the `return` statement:

```tsx title="src/app/[countryCode]/(main)/account/@dashboard/profile/page.tsx" badgeLabel="Storefront" badgeColor="blue"
<div className="flex flex-col gap-y-8 w-full">
  {/* ... */}
  {/* Remove the following */}
  <Divider />
  <ProfileEmail customer={customer} />
  <Divider />
  <ProfilePhone customer={customer} />
  {/* ... */}
</div>
```

If you go to your profile page and click on "Profile" in the sidebar, the email and phone number sections will be removed.

![Profile page without email and phone fields](https://res.cloudinary.com/dza7lstvk/image/upload/v1747740515/Medusa%20Resources/CleanShot_2025-05-20_at_14.28.09_2x_ugab7d.png)

### c. Disable Phone Updates in Medusa

While removing the email and phone fields from the profile page prevents customers using the storefront from updating their phone number, it doesn't prevent them from updating it using Medusa's API.

In this section, you'll add a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) to the `/store/customers/me` API route that prevents customers from updating their phone number.

A middleware is a function that's executed whenever a request is sent to an API route. It's executed before the route handler, allowing you to validate requests, apply authentication guards, and more.

Learn more in the [Middlewares](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) documentation.

To add a middleware in your Medusa application, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" badgeLabel="Medusa Application" badgeColor="green"
import { defineMiddlewares } from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/store/customers/me",
      method: ["POST"],
      middlewares: [
        async (req, res, next) => {
          const { phone } = req.body as Record<string, string>

          if (phone) {
            return res.status(400).json({
              error: "Phone number is not allowed to be updated",
            })
          }

          next()
        },
      ],
    },
  ],
})
```

You define middlewares using the `defineMiddlewares` function. It accepts an object having a `routes` property that holds all middlewares applied to API routes.

Each object in `routes` has the following properties:

- `matcher`: The API route path to apply the middleware on. You set it to `/store/customers/me`.
- `method`: The HTTP method to apply the middleware to. You set it to `POST` so that the middleware is applied only on `POST` requests sent to the `/store/customers/me` route.
- `middlewares`: An array of middlewares to apply on the route. You add a middleware that throws an error if the request body contains a `phone` property. This prevents customers from updating their phone number using the API.

Any `POST` request to the `/store/customers/me` route will now be validated to ensure it's not updating the phone number.

***

## Next Steps

You've now implemented phone authentication in Medusa with Twilio integration. You can further customize the phone authentication feature based on your business use case.

### Authenticate Other Actor Types

This tutorial focused on authenticating customers using their phone number. However, you can also authenticate other actor types, such as admin users and vendors.

To do that, first, enable the `phone-auth` authentication strategy in `medusa-config.ts` for the actor types. For example:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    // ...
    http: {
      // ...
      authMethodsPerActor: {
        user: ["emailpass", "phone-auth"],
        customer: ["emailpass", "phone-auth"],
        vendor: ["emailpass", "phone-auth"],
      },
    },
  },
})
```

Then, when sending requests to the authentication API routes mentioned in the [Test Out Phone Authentication](#test-out-phone-authentication) section, replace `customer` in the API route paths with the actor type you want to authenticate:

- `/auth/customer/phone-auth/register` -> `/auth/user/phone-auth/register`
- `/auth/customer/phone-auth` -> `/auth/user/phone-auth`
- `/auth/customer/phone-auth/callback` -> `/auth/user/phone-auth/callback`

Finally, update the UI to show the phone authentication option for the actor type you want to authenticate. This depends on the UI you're using, but you can follow an approach similar to the Next.js Starter Storefront customizations.

The login form of Medusa Admin can't be customized, so you'll have to build a custom admin dashboard to support phone authentication for admin users.

### Learn More about Medusa

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.
3. Contact the [sales team](https://medusajs.com/contact/) to get help from the Medusa team.


# Implement Pre-Orders in Medusa

In this tutorial, you'll learn how to implement pre-orders in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) that are available out-of-the-box.

The pre-orders feature allows customers to purchase a product before it's available for delivery. Once the product is available, it's automatically shipped to the customer. This is useful when a business is launching and marketing a new product, such as merchandise or books.

This tutorial will help you implement pre-orders in your Medusa application.

## Summary

By following this tutorial, you will learn how to:

- Install and set up Medusa.
- Define the data models necessary for pre-orders and the logic to manage them.
- Customize the Medusa Admin dashboard to allow merchants to:
  - Manage pre-order configurations of product variants.
  - View pre-orders.
- Automate fulfilling pre-orders when the product becomes available.
- Handle scenarios where a pre-order is canceled or modified.

![Diagram showcasing the pre-order features from this tutorial](https://res.cloudinary.com/dza7lstvk/image/upload/v1752756906/Medusa%20Resources/pre-order-summary_epvssb.jpg)

- [Pre-order Repository](https://github.com/medusajs/examples/tree/main/preorders): Find the full code for this guide in this repository.
- [OpenAPI Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1752672124/OpenApi/PreOrder_nchc1w.yaml): Import this OpenAPI Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.

Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Preorder Module

In Medusa, you can build custom features in a [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a reusable package with the data models and functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

In this step, you'll build a Preorder Module that defines the data models and logic to manage pre-orders. Later, you'll build commerce flows related to pre-orders around the module.

Refer to the [Modules documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to learn more.

### a. Create Module Directory

Create the directory `src/modules/preorder` that will hold the Preorder Module's code.

### b. Create Data Models

A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

Refer to the [Data Models documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md) to learn more.

For the Preorder Module, you'll define a data model to represent pre-order configurations for a product variant, and another to represent a pre-order placed by a customer.

#### PreorderVariant Data Model

To create the first data model, create the file `src/modules/preorder/models/preorder-variant.ts` with the following content:

```ts title="src/modules/preorder/models/preorder-variant.ts" highlights={preorderVariantHighlights}
import { model } from "@medusajs/utils"
import { Preorder } from "./preorder"

export enum PreorderVariantStatus {
  ENABLED = "enabled",
  DISABLED = "disabled",
}

export const PreorderVariant = model.define(
  "preorder_variant",
  {
    id: model.id().primaryKey(),
    variant_id: model.text().unique(),
    available_date: model.dateTime().index(),
    status: model.enum(Object.values(PreorderVariantStatus))
      .default(PreorderVariantStatus.ENABLED),
    preorders: model.hasMany(() => Preorder, {
      mappedBy: "item",
    }),
  }
)
```

The `PreorderVariant` data model has the following properties:

- `id`: The primary key of the table.
- `variant_id`: The ID of the product variant that this pre-order variant configurations applies to.
  - Later, you'll learn how to link this data model to Medusa's `ProductVariant` data model.
- `available_date`: The date when the product variant will be available for delivery.
- `status`: The status of the pre-order variant configuration, which can be either `enabled` or `disabled`.
- `preorders`: A relation to the `Preorder` data model, which you'll create next.

Data relevant for pre-orders like price, inventory, etc... are all either included in the `ProductVariant` data model or its linked records. So, you don't need to duplicate this information in the `PreorderVariant` data model.

Learn more about defining data model properties in the [Property Types documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md).

#### Preorder Data Model

Next, you'll create the `Preorder` data model that represents a customer's purchase of a pre-order product variant.

Create the file `src/modules/preorder/models/preorder.ts` with the following content:

```ts title="src/modules/preorder/models/preorder.ts" highlights={preorderHighlights}
import { model } from "@medusajs/utils"
import { PreorderVariant } from "./preorder-variant"

export enum PreorderStatus {
  PENDING = "pending",
  FULFILLED = "fulfilled",
  CANCELLED = "cancelled",
}

export const Preorder = model.define(
  "preorder",
  {
    id: model.id().primaryKey(),
    order_id: model.text().index(),
    item: model.belongsTo(() => PreorderVariant, {
      mappedBy: "preorders",
    }),
    status: model.enum(Object.values(PreorderStatus))
      .default(PreorderStatus.PENDING),
  }
).indexes([
  {
    on: ["item_id", "status"],
  },
])
```

The `Preorder` data model has the following properties:

- `id`: The primary key of the table.
- `order_id`: The ID of the Medusa order that this pre-order belongs to.
  - Later, you'll learn how to link this data model to Medusa's `Order` data model.
- `item`: A relation to the `PreorderVariant` data model, which represents the item that was pre-ordered.
- `status`: The status of the pre-order, which can be either `pending`, `fulfilled`, or `cancelled`.

You also define an index on the `item_id` and `status` columns to optimize queries that filter by these properties.

### c. Create Module's Service

You can manage your module's data models in a service.

A service is a TypeScript class that the module exports. In the service's methods, you can connect to the database, allowing you to manage your data models, or connect to a third-party service, which is useful if you're integrating with external services.

Refer to the [Module Service documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md) to learn more.

To create the Preorder Module's service, create the file `src/modules/preorder/services/preorder.ts` with the following content:

```ts title="src/modules/preorder/services/preorder.ts"
import { MedusaService } from "@medusajs/framework/utils"
import { PreorderVariant } from "./models/preorder-variant"
import { Preorder } from "./models/preorder"

export default class PreorderModuleService extends MedusaService({
  PreorderVariant,
  Preorder,
}) {}
```

The `PreorderModuleService` extends `MedusaService`, which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods.

So, the `PreorderModuleService` class now has methods like `createPreorders` and `retrievePreorder`.

Find all methods generated by the `MedusaService` in [the Service Factory reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md).

### d. Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/preorder/index.ts` with the following content:

```ts title="src/modules/preorder/index.ts"
import { Module } from "@medusajs/framework/utils"
import PreorderModuleService from "./service"

export const PREORDER_MODULE = "preorder"

export default Module(PREORDER_MODULE, {
  service: PreorderModuleService,
})
```

You use the `Module` function to create the module's definition. It accepts two parameters:

1. The module's name, which is `preorder`.
2. An object with a required property `service` indicating the module's service.

You also export the module's name as `PREORDER_MODULE` so you can reference it later.

### e. Add Module to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/preorder",
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

### f. Generate Migrations

Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript class that defines database changes made by a module.

Refer to the [Migrations documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md) to learn more.

Medusa's CLI tool can generate the migrations for you. To generate a migration for the Preorder Module, run the following command in your Medusa application's directory:

```bash
npx medusa db:generate preorder
```

The `db:generate` command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a `migrations` directory under `src/modules/preorder` that holds the generated migration.

Then, to reflect these migrations on the database, run the following command:

```bash
npx medusa db:migrate
```

The tables for the `PreorderVariant` and `Preorder` data models are now created in the database.

***

## Step 3: Link Preorder and Medusa Data Models

Since Medusa isolates modules to integrate them into your application without side effects, you can't directly create relationships between data models of different modules.

Instead, Medusa provides a mechanism to define links between data models, and retrieve and manage linked records while maintaining module isolation.

Refer to the [Module Isolation documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md) to learn more.

In this step, you'll define a link between the data models in the Preorder Module and the data models in Medusa's Commerce Modules.

### a. PreorderVariant \<> ProductVariant Link

To define a link between the `PreorderVariant` and `ProductVariant` data models, create the file `src/links/preorder-variant.ts` with the following content:

```ts title="src/links/preorder-variant.ts"
import { defineLink } from "@medusajs/framework/utils"
import PreorderModule from "../modules/preorder"
import ProductModule from "@medusajs/medusa/product"

export default defineLink(
  PreorderModule.linkable.preorderVariant,
  ProductModule.linkable.productVariant
)
```

You define a link using the `defineLink` function. It accepts two parameters:

1. An object indicating the first data model part of the link. A module has a special `linkable` property that contains link configurations for its data models. You pass the linkable configurations of the Preorder Module's `PreorderVariant` data model.
2. An object indicating the second data model part of the link. You pass the linkable configurations of the Product Module's `ProductVariant` data model.

In later steps, you'll learn how this link allows you to retrieve and manage product variants and their pre-order configurations.

Refer to the [Module Links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md) documentation to learn more about defining links.

### b. Preorder \<> Order Link

Next, you'll define a read-only link between the `Preorder` data model and Medusa's `Order` data model. Read-only links allow you to retrieve the order that a pre-order belongs to without the need to manage the link in the database.

Create the file `src/links/preorder-order.ts` with the following content:

```ts title="src/links/preorder-order.ts"
import { defineLink } from "@medusajs/framework/utils"
import PreorderModule from "../modules/preorder"
import OrderModule from "@medusajs/medusa/order"

export default defineLink(
  {
    linkable: PreorderModule.linkable.preorder,
    field: "order_id",
  },
  OrderModule.linkable.order,
  {
    readOnly: true,
  }
)
```

You define the link in a similar way to the previous one, passing three parameters to the `defineLink` function:

1. The first data model part of the link, which is the `Preorder` data model. You also pass a `field` property that indicates the column in the `Preorder` data model that holds the ID of the linked `Order`.
2. The second data model part of the link, which is the `Order` data model.
3. An object that has a `readOnly` property, marking this link as read-only.

In later steps, you'll learn how to use this link to retrieve the order that a pre-order belongs to.

### c. Sync Links to Database

After defining links, you need to sync them to the database. This creates the necessary tables to manage the links.

This doesn't apply to read-only links, as they don't require database changes.

To sync the links to the database, run the migrations command again in the Medusa application's directory:

```bash
npx medusa db:migrate
```

This command will create the necessary table to manage the `PreorderVariant` \<> `ProductVariant` link.

***

## Step 4: Manage Pre-Order Variant Functionalities

In this step, you'll implement the logic to manage pre-order configurations of product variants. This includes creating, updating, and disabling pre-order configurations.

When you build commerce features in Medusa that can be consumed by client applications, such as the Medusa Admin dashboard or a storefront, you need to implement:

1. A [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) with steps that define the business logic of the feature.
2. An [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) that exposes the workflow's functionality to client applications.

In this step, you'll implement the workflows and API routes to manage pre-order configurations of product variants.

### a. Upsert Pre-Order Variant Workflow

The first workflow you'll implement allows merchants to create or update pre-order configurations of product variants.

A workflow is a series of queries and actions, called steps, that complete a task. A workflow is similar to a function, but it allows you to track its executions' progress, define roll-back logic, and configure other advanced features.

Refer to the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) to learn more.

The workflow you'll build will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Confirm that the product variant exists.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Try to retrieve existing pre-order configurations.

The `useQueryGraphStep` and `createRemoteLinkStep` are available through Medusa's `@medusajs/medusa/core-flows` package. You'll implement other steps in the workflow.

#### updatePreorderVariantStep

The `updatePreorderVariantStep` updates an existing pre-order variant.

To create the step, create the file `src/workflows/steps/update-preorder-variant.ts` with the following content:

If you get a type error on resolving the Preorder Module, run the Medusa application once with the `npm run dev` or `yarn dev` command to generate the necessary type definitions, as explained in the [Automatically Generated Types guide](https://docs.medusajs.com/docs/learn/fundamentals/generated-types/index.html.md).

```ts title="src/workflows/steps/update-preorder-variant.ts" highlights={updatePreorderVariantStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PREORDER_MODULE } from "../../modules/preorder"
import { 
  PreorderVariantStatus,
} from "../../modules/preorder/models/preorder-variant"

type StepInput = {
  id: string
  variant_id?: string
  available_date?: Date
  status?: PreorderVariantStatus
}

export const updatePreorderVariantStep = createStep(
  "update-preorder-variant",
  async (input: StepInput, { container }) => {
    const preorderModuleService = container.resolve(
      PREORDER_MODULE
    )
    
    const oldData = await preorderModuleService.retrievePreorderVariant(
      input.id
    )
    
    const preorderVariant = await preorderModuleService.updatePreorderVariants(
      input
    )

    return new StepResponse(preorderVariant, oldData)
  },
  async (preorderVariant, { container }) => {
    if (!preorderVariant) {
      return
    }

    const preorderModuleService = container.resolve(
      PREORDER_MODULE
    )

    await preorderModuleService.updatePreorderVariants({
      id: preorderVariant.id,
      variant_id: preorderVariant.variant_id,
      available_date: preorderVariant.available_date,
    })
  }
)
```

You create a step with the `createStep` function. It accepts three parameters:

1. The step's unique name.
2. An async function that receives two parameters:
   - The step's input, which is an object with the pre-order variant's properties.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.
3. An async compensation function that undoes the actions performed by the step function. This function is only executed if an error occurs during the workflow's execution.

In the step function, you resolve the Preorder Module's service from the Medusa container using its `resolve` method, passing it the module's name as a parameter.

Then, you retrieve the existing configurations to undo the updates if an error occurs during the workflow's execution.

After that, you update the preorder variant using the generated `updatePreorderVariants` method of the Preorder Module's service.

Finally, a step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters:

1. The step's output, which is the pre-order variant created.
2. Data to pass to the step's compensation function.

In the compensation function, you undo the updates made to the preorder variant if an error occurs in the workflow.

#### createPreorderVariantStep

The `createPreorderVariantStep` creates a pre-order variant record.

To create the step, create the file `src/workflows/steps/create-preorder-variant.ts` with the following content:

```ts title="src/workflows/steps/create-preorder-variant.ts" highlights={createPreorderVariantStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PREORDER_MODULE } from "../../modules/preorder"

type StepInput = {
  variant_id: string
  available_date: Date
}

export const createPreorderVariantStep = createStep(
  "create-preorder-variant",
  async (input: StepInput, { container }) => {
    const preorderModuleService = container.resolve(
      PREORDER_MODULE
    )

    const preorderVariant = await preorderModuleService.createPreorderVariants(
      input
    )

    return new StepResponse(preorderVariant, preorderVariant.id)
  },
  async (preorderVariantId, { container }) => {
    if (!preorderVariantId) {
      return
    }

    const preorderModuleService = container.resolve(
      PREORDER_MODULE
    )

    await preorderModuleService.deletePreorderVariants(preorderVariantId)
  }
)
```

In the step, you create a preorder variant record using the data received as input.

In the step's compensation function, you delete the preorder variant if an error occurs in the workflow's execution.

#### Create Workflow

You now have the necessary steps to build the workflow that upserts a preorder variant's configurations.

To create the workflow, create the file `src/workflows/upsert-product-variant-preorder.ts` with the following content:

```ts title="src/workflows/upsert-product-variant-preorder.ts" highlights={upsertProductVariantPreorderWorkflowHighlights}
import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { createRemoteLinkStep, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { updatePreorderVariantStep } from "./steps/update-preorder-variant"
import { createPreorderVariantStep } from "./steps/create-preorder-variant"
import { PREORDER_MODULE } from "../modules/preorder"
import { Modules } from "@medusajs/framework/utils"
import { PreorderVariantStatus } from "../modules/preorder/models/preorder-variant"

type WorkflowInput = {
  variant_id: string;
  available_date: Date;
}

export const upsertProductVariantPreorderWorkflow = createWorkflow(
  "upsert-product-variant-preorder",
  (input: WorkflowInput) => {
    // confirm that product variant exists
    useQueryGraphStep({
      entity: "product_variant",
      fields: ["id"],
      filters: {
        id: input.variant_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const { data: preorderVariants } = useQueryGraphStep({
      entity: "preorder_variant",
      fields: ["*"],
      filters: {
        variant_id: input.variant_id,
      },
    }).config({ name: "retrieve-preorder-variant" })

    const updatedPreorderVariant = when(
      { preorderVariants }, 
      (data) => data.preorderVariants.length > 0
    ).then(() => {
      const preorderVariant = updatePreorderVariantStep({
        id: preorderVariants[0].id,
        variant_id: input.variant_id,
        available_date: input.available_date,
        status: PreorderVariantStatus.ENABLED,
      })

      return preorderVariant
    })

    const createdPreorderVariant = when(
      { preorderVariants }, 
      (data) => data.preorderVariants.length === 0
    ).then(() => {
      const preorderVariant = createPreorderVariantStep(input)
      
      createRemoteLinkStep([
        {
          [PREORDER_MODULE]: {
            preorder_variant_id: preorderVariant.id,
          },
          [Modules.PRODUCT]: {
            product_variant_id: preorderVariant.variant_id,
          },
        },
      ])

      return preorderVariant
    })

    const preorderVariant = transform({
      updatedPreorderVariant,
      createdPreorderVariant,
    }, (data) => 
      data.createdPreorderVariant || data.updatedPreorderVariant
    )

    return new WorkflowResponse(preorderVariant)
  }
)
```

You create a workflow using the `createWorkflow` function. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function that holds the workflow's implementation. The function accepts an input object holding the variant's ID and its available date.

In the workflow, you:

1. Try to retrieve the variant's details to confirm it exists. The [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md) uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) which allows you to retrieve data across modules.
   - If it doesn't exist, an error is thrown and the workflow will stop executing.
2. Try to retrieve the variant's existing preorder configurations, if any.
3. Use [when-then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) to check if there are existing pre-order configurations.
   - If so, you update the pre-order variant record.
4. Use `when-then` to check if there are no existing pre-order configurations.
   - If so, you create a pre-order variant record and link it to the product variant.
5. Use `transform` to return either the created or updated preorder variant.

A workflow must return an instance of `WorkflowResponse` that accepts the data to return to the workflow's executor.

`when-then` and `transform` allow you to access the values of data during execution. Learn more in the [Data Manipulation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) and [When-Then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) documentation.

### b. Upsert Pre-Order Variant API Route

Next, you'll create the API route that exposes the workflow's functionality to clients.

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

Refer to the [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) documentation to learn more about them.

Create the file `src/api/admin/variants/[id]/preorders/route.ts` with the following content:

```ts title="src/api/admin/variants/[id]/preorders/route.ts" highlights={upsertPreorderVariantRouteHighlights}
import { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { 
  upsertProductVariantPreorderWorkflow, 
} from "../../../../../workflows/upsert-product-variant-preorder"
import { z } from "zod"

export const UpsertPreorderVariantSchema = z.object({
  available_date: z.string().datetime(),
})

type UpsertPreorderVariantSchema = z.infer<
  typeof UpsertPreorderVariantSchema
>

export const POST = async (
  req: AuthenticatedMedusaRequest<UpsertPreorderVariantSchema>,
  res: MedusaResponse
) => {
  const variantId = req.params.id

  const { result } = await upsertProductVariantPreorderWorkflow(req.scope)
    .run({
      input: {
        variant_id: variantId,
        available_date: new Date(req.validatedBody.available_date),
      },
    })

  res.json({
    preorder_variant: result,
  })
}
```

You create the `UpsertPreorderVariantSchema` schema that is used to validate request bodies sent to this API route with [Zod](https://zod.dev/).

Then, you export a `POST` function, which will expose a `POST` API route at `/admin/variants/:id/preorders`.

In the API route, you execute the `upsertProductVariantPreorderWorkflow`, passing it the variant ID from the path parameter and the availability date from the request body.

Finally, you return the created or updated pre-order variant record in the response.

#### Add Validation Middleware

To validate the body parameters of requests sent to the API route, you need to apply a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

To apply a middleware to a route, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" highlights={middlewaresHighlights}
import { 
  defineMiddlewares, 
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { 
  UpsertPreorderVariantSchema,
} from "./admin/variants/[id]/preorders/route"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/admin/variants/:id/preorders",
      methods: ["POST"],
      middlewares: [
        validateAndTransformBody(UpsertPreorderVariantSchema),
      ],
    },
  ],
})
```

You apply Medusa's `validateAndTransformBody` middleware to `POST` requests sent to the `/admin/variants/:id/preorders` API route.

The middleware function accepts a Zod schema, which you created in the API route's file.

Refer to the [Middlewares](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) documentation to learn more.

You'll test this API route later when you customize the Medusa Admin.

### c. Disable Pre-Order Variant Workflow

Next, you'll create a workflow that will disable the pre-order variant configuration. This is useful if the merchant doesn't want the variant to be preorderable anymore.

This workflow has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the pre-order configurations of a variant.
- [disablePreorderVariantStep](#disablePreorderVariantStep): Disable the pre-order variant configurations.

You only need to implement the `disablePreorderVariantStep`.

#### disablePreorderVariantStep

The `disablePreorderVariantStep` changes the status of a pre-order variant record to `disabled`.

Create the file `src/workflows/steps/disable-preorder-variant.ts` with the following content:

```ts title="src/workflows/steps/disable-preorder-variant.ts" highlights={disablePreorderVariantStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PREORDER_MODULE } from "../../modules/preorder"
import { 
  PreorderVariantStatus,
} from "../../modules/preorder/models/preorder-variant"

type StepInput = {
  id: string
}

export const disablePreorderVariantStep = createStep(
  "disable-preorder-variant",
  async ({ id }: StepInput, { container }) => {
    const preorderModuleService = container.resolve(PREORDER_MODULE)

    const oldData = await preorderModuleService.retrievePreorderVariant(id)
    
    const preorderVariant = await preorderModuleService.updatePreorderVariants({
      id,
      status: PreorderVariantStatus.DISABLED,
    })

    return new StepResponse(preorderVariant, oldData)
  },
  async (preorderVariant, { container }) => {
    if (!preorderVariant) {
      return
    }

    const preorderModuleService = container.resolve(PREORDER_MODULE)

    await preorderModuleService.updatePreorderVariants({
      id: preorderVariant.id,
      status: preorderVariant.status,
    })
  }
)
```

The step receives the ID of the pre-order variant, and changes its status to `disabled`.

In the compensation function, you revert the pre-order variant's status to its previous value.

#### Create Workflow

To create the workflow that disables the pre-order variant configuration, create the file `src/workflows/disable-preorder-variant.ts` with the following content:

```ts title="src/workflows/disable-preorder-variant.ts" highlights={disablePreorderVariantWorkflowHighlights}
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { disablePreorderVariantStep } from "./steps/disable-preorder-variant"

type WorkflowInput = {
  variant_id: string
}

export const disablePreorderVariantWorkflow = createWorkflow(
  "disable-preorder-variant",
  (input: WorkflowInput) => {
    const { data: preorderVariants } = useQueryGraphStep({
      entity: "preorder_variant",
      fields: ["*"],
      filters: {
        variant_id: input.variant_id,
      },
    })

    const preorderVariant = disablePreorderVariantStep({
      id: preorderVariants[0].id,
    })

    return new WorkflowResponse(preorderVariant)
  }
)
```

In the workflow, you:

1. Retrieve the pre-order variant configuration.
2. Disable the pre-order variant configuration.

You return the updated pre-order variant record.

### d. Disable Pre-Order Variant API Route

To expose the functionality to disable a variant's pre-order configurations, you'll create an API route that executes the workflow.

In the file `src/api/admin/variants/[id]/preorders/route.ts`, add the following import at the top of the file:

```ts title="src/api/admin/variants/[id]/preorders/route.ts"
import { 
  disablePreorderVariantWorkflow,
} from "../../../../../workflows/disable-preorder-variant"
```

Then, add the following function at the end of the file:

```ts title="src/api/admin/variants/[id]/preorders/route.ts"
export const DELETE = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const variantId = req.params.id

  const { result } = await disablePreorderVariantWorkflow(req.scope).run({
    input: {
      variant_id: variantId,
    },
  })

  res.json({
    preorder_variant: result,
  })
}
```

You expose a `DELETE` API route at `/admin/variants/:id/preorders`.

In the API route, you execute the `disablePreorderVariantWorkflow` and return the updated preorder variant record.

You'll test out this functionality when you customize the Medusa Admin in the next step.

***

## Step 5: Add Widget in Product Variant Admin Page

In this step, you'll customize the Medusa Admin to allow admin users to manage pre-order configurations of product variants.

The Medusa Admin dashboard is customizable, allowing you to insert widgets into existing pages, or create new pages.

Refer to the [Admin Development](https://docs.medusajs.com/docs/learn/fundamentals/admin/index.html.md) documentation to learn more.

In this step, you'll insert a widget into the product variant page that allows admin users to manage pre-order configurations.

### a. Initialize JS SDK

To send requests to the Medusa server, you'll use the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md). It's already installed in your Medusa project, but you need to initialize it before using it in your customizations.

Create the file `src/admin/lib/sdk.ts` with the following content:

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

Learn more about the initialization options in the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) reference.

### b. Define Types

Next, you'll define types that you'll use in your admin customizations.

Create the file `src/admin/lib/types.ts` with the following content:

```ts title="src/admin/lib/types.ts" highlights={typesHighlights}
import { HttpTypes } from "@medusajs/framework/types"

export interface PreorderVariant {
  id: string
  variant_id: string
  available_date: string
  status: "enabled" | "disabled"
  created_at: string
  updated_at: string
}

export interface Preorder {
  id: string
  order_id: string
  status: "pending" | "fulfilled" | "cancelled"
  created_at: string
  updated_at: string
  item: PreorderVariant & {
    product_variant?: HttpTypes.AdminProductVariant
  }
  order?: HttpTypes.AdminOrder
}

export interface PreordersResponse {
  preorders: Preorder[]
}

export interface PreorderVariantResponse {
  variant: HttpTypes.AdminProductVariant & {
    preorder_variant?: PreorderVariant
  }
}

export interface CreatePreorderVariantData {
  available_date: Date
}
```

You define types for pre-order variants, pre-orders, and request and response types.

### c. Define Pre-order Variant Hook

To send requests to the Medusa server with support for caching and refetching capabilities, you'll wrap JS SDK methods with [Tanstack (React) Query](https://tanstack.com/query/latest).

So, you'll create a hook that exposes queries and mutations to manage pre-order configurations.

Refer to the [Admin Development Tips](https://docs.medusajs.com/docs/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md) documentation to learn more.

Create the file `src/admin/hooks/use-preorder-variant.ts` with the following content:

```ts title="src/admin/hooks/use-preorder-variant.ts" highlights={usePreorderVariantHighlights}
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { toast } from "@medusajs/ui"
import { sdk } from "../lib/sdk"
import { PreorderVariantResponse, CreatePreorderVariantData } from "../lib/types"
import { HttpTypes } from "@medusajs/framework/types"

export const usePreorderVariant = (variant: HttpTypes.AdminProductVariant) => {
  const queryClient = useQueryClient()

  const { data, isLoading, error } = useQuery<PreorderVariantResponse>({
    queryFn: () => sdk.admin.product.retrieveVariant(
      variant.product_id!,
      variant.id,
      { fields: "*preorder_variant" }
    ),
    queryKey: ["preorder-variant", variant.id],
  })

  const upsertMutation = useMutation({
    mutationFn: async (data: CreatePreorderVariantData) => {
      return sdk.client.fetch(`/admin/variants/${variant.id}/preorders`, {
        method: "POST",
        body: data,
      })
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["preorder-variant", variant.id] })
      toast.success("Preorder configuration saved successfully")
    },
    onError: (error) => {
      toast.error(`Failed to save preorder configuration: ${error.message}`)
    },
  })

  const disableMutation = useMutation({
    mutationFn: async () => {
      return sdk.client.fetch(`/admin/variants/${variant.id}/preorders`, {
        method: "DELETE",
      })
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["preorder-variant", variant.id] })
      toast.success("Preorder configuration removed successfully")
    },
    onError: (error) => {
      toast.error(`Failed to remove preorder configuration: ${error.message}`)
    },
  })

  return {
    preorderVariant: data?.variant.preorder_variant?.status === "enabled" ? 
      data.variant.preorder_variant : null,
    isLoading,
    error,
    upsertPreorder: upsertMutation.mutate,
    disablePreorder: disableMutation.mutate,
    isUpserting: upsertMutation.isPending,
    isDisabling: disableMutation.isPending,
  }
}
```

The `usePreorderVariant` hook returns an object with the following properties:

- `preorderVariant`: The pre-order variant. It's retrieved from Medusa's [Retrieve Variant](https://docs.medusajs.com/api/admin#products_getproductsidvariantsvariant_id) API route, which allows expanding linked records using the `fields` query parameter.
- `isLoading`: Whether the pre-order variant is being retrieved.
- `error`: Errors that occur while retrieving the pre-order variant.
- `upsertPreorder`: A mutation to create or update a pre-order variant.
- `disablePreorder`: A mutation to disable a pre-order variant.
- `isUpserting`: Whether the pre-order variant is being created or updated.
- `isDisabling`: Whether the pre-order variant is being disabled.

### d. Define Pre-order Variant Widget

You can now add the widget that allows admin users to manage pre-order configurations of product variants.

Widgets are created in a `.tsx` file under the `src/admin/widgets` directory. So, create the file `src/admin/widgets/preorder-variant-widget.tsx` with the following content:

```tsx title="src/admin/widgets/preorder-variant-widget.tsx" collapsibleLines="1-23" expandButtonLabel="Show Imports" highlights={preorderVariantWidgetHighlights}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { 
  Container, 
  Heading, 
  Text, 
  Button, 
  Drawer,
  Label,
  DatePicker,
  DropdownMenu,
  IconButton,
  clx,
  usePrompt,
  StatusBadge,
} from "@medusajs/ui"
import { useEffect, useState } from "react"
import { 
  DetailWidgetProps, 
  AdminProductVariant,
} from "@medusajs/framework/types"
import { Calendar, EllipsisHorizontal, Pencil, Plus, XCircle } from "@medusajs/icons"
import { usePreorderVariant } from "../hooks/use-preorder-variant"

const PreorderWidget = ({ 
  data: variant,
}: DetailWidgetProps<AdminProductVariant>) => {
  const [isDrawerOpen, setIsDrawerOpen] = useState(false)
  const [availableDate, setAvailableDate] = useState(
    new Date().toString()
  )

  const dialog = usePrompt()

  const {
    preorderVariant,
    isLoading,
    upsertPreorder: createPreorder,
    disablePreorder: deletePreorder,
    isUpserting: isCreating,
    isDisabling,
  } = usePreorderVariant(variant)

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    if (!availableDate) {
      return
    }

    createPreorder(
      { available_date: new Date(availableDate) },
      {
        onSuccess: () => {
          setIsDrawerOpen(false)
          setAvailableDate(new Date().toString())
        },
      }
    )
  }

  const handleDisable = async () => {
    const confirmed = await dialog({
      title: "Are you sure?",
      description: "This will remove the preorder configuration for this variant. Any existing preorders will not be automatically fulfilled.",
      variant: "danger",
    })
    if (confirmed) {
      deletePreorder()
    }
  }

  const formatDate = (dateString: string) => {
    return new Date(dateString).toLocaleDateString("en-US", {
      year: "numeric",
      month: "long",
      day: "numeric",
    })
  }

  useEffect(() => {
    if (preorderVariant) {
      setAvailableDate(preorderVariant.available_date)
    } else {
      setAvailableDate(new Date().toString())
    }
  }, [preorderVariant])

  // TODO add a return statement
}

export const config = defineWidgetConfig({
  zone: "product_variant.details.side.after",
})

export default PreorderWidget
```

A widget file must export:

- A default React component. This component renders the widget's UI.
- A `config` object created with `defineWidgetConfig` from the Admin SDK. It accepts an object with the `zone` property that indicates where the widget will be rendered in the Medusa Admin dashboard.

In the widget's component, you retrieve the preorder variant using the `usePreorderVariant` hook.

You also add a function to handle saving the pre-order configuration, and another to handle disabling the pre-order configuration.

Next, you need to render the widget's UI. Add the following return statement at the end of the widget's component:

```tsx title="src/admin/widgets/preorder-variant-widget.tsx"
const PreorderWidget = ({ 
  data: variant,
}: DetailWidgetProps<AdminProductVariant>) => {
  // ...
  return (
    <>
      <Container className="divide-y p-0">
        <div className="flex items-center justify-between px-6 py-4">
          <div className="flex items-center gap-2">
            <Heading level="h2">Pre-order</Heading>
            {preorderVariant?.status === "enabled" && (
              <StatusBadge color={"green"}>
                Enabled
              </StatusBadge>
            )}
          </div>
          <DropdownMenu>
            <DropdownMenu.Trigger asChild>
              <IconButton size="small" variant="transparent">
                <EllipsisHorizontal />
              </IconButton>
            </DropdownMenu.Trigger>
            <DropdownMenu.Content>
              <DropdownMenu.Item
                disabled={isCreating || isDisabling}
                onClick={() => setIsDrawerOpen(true)}
                className={clx(
                  "[&_svg]:text-ui-fg-subtle flex items-center gap-x-2",
                  {
                    "[&_svg]:text-ui-fg-disabled": isCreating || isDisabling,
                  }
                )}
              >
                { preorderVariant ? <Pencil /> : <Plus />}
                <span>
                  { preorderVariant ? "Edit" : "Add" } Pre-order Configuration
                </span>
              </DropdownMenu.Item>
              <DropdownMenu.Item
                disabled={isCreating || isDisabling || !preorderVariant}
                onClick={handleDisable}
                className={clx(
                  "[&_svg]:text-ui-fg-subtle flex items-center gap-x-2",
                  {
                    "[&_svg]:text-ui-fg-disabled": isCreating || isDisabling || !preorderVariant,
                  }
                )}
              >
                <XCircle />
                <span>
                  Remove Pre-order Configuration
                </span>
              </DropdownMenu.Item>
            </DropdownMenu.Content>

          </DropdownMenu>
        </div>
        
        <div className="px-6 py-4">
          {isLoading ? (
            <Text>Loading pre-order information...</Text>
          ) : preorderVariant ? (
            <div className="space-y-3">
              <div className="flex items-center gap-2 text-ui-fg-subtle">
                <Calendar className="w-4 h-4" />
                <Text size="small">
                  Available: {formatDate(preorderVariant.available_date)}
                </Text>
              </div>
            </div>
          ) : (
            <div className="text-center py-4">
              <Text className="text-ui-fg-subtle">
                This variant is not configured for pre-order
              </Text>
              <Text size="small" className="text-ui-fg-muted mt-1">
                Set up pre-order configuration to allow customers to order the product variant, then automatically fulfill it when it becomes available.
              </Text>
            </div>
          )}
        </div>
      </Container>

      <Drawer open={isDrawerOpen} onOpenChange={setIsDrawerOpen}>
        <Drawer.Content>
          <Drawer.Header>
            <Drawer.Title>
              {preorderVariant ? "Edit" : "Add"} Pre-order Configuration
            </Drawer.Title>
          </Drawer.Header>
          
          <Drawer.Body>
            <form onSubmit={handleSubmit} className="flex flex-col gap-4">
              <div className="space-y-2">
                <Label htmlFor="available-date">Available Date</Label>
                <DatePicker
                  id="available-date"
                  value={new Date(availableDate)}
                  onChange={(date) => setAvailableDate(date?.toString() || "")}
                  minValue={new Date()}
                  isRequired={true}
                />
                <Text size="small" className="text-ui-fg-subtle">
                  Customers can pre-order this variant until this date, when it becomes available for regular purchase.
                </Text>
              </div>
            </form>
          </Drawer.Body>

          <Drawer.Footer>
            <Button 
              variant="secondary" 
              onClick={() => setIsDrawerOpen(false)}
              disabled={isCreating}
            >
              Cancel
            </Button>
            <Button 
              type="submit"
              onClick={handleSubmit}
              isLoading={isCreating}
            >
              Save
            </Button>
          </Drawer.Footer>
        </Drawer.Content>
      </Drawer>
    </>
  )
}
```

You render the variant's pre-order configurations if available. You also give the admin user the option to add or edit pre-order configurations and remove (disable) them.

To show the pre-order configuration form, you use the [Drawer](https://docs.medusajs.com/ui/components/drawer/index.html.md) component from Medusa UI.

### Test the Customizations

You can now test the customizations you made in the Medusa server and admin dashboard.

Start the Medusa application with the following command:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and log in.

Go to any product, then click on one of its variants. You'll find a new "Pre-order" section in the side column.

![Pre-order variant widget in the Medusa Admin dashboard](https://res.cloudinary.com/dza7lstvk/image/upload/v1752735553/Medusa%20Resources/CleanShot_2025-07-17_at_09.52.30_2x_ftojti.png)

To add pre-order configuration using the widget:

1. Click on the <InlineIcon Icon={EllipsisHorizontal} alt="three-dots" /> icon in the top right corner of the widget.
2. Click on "Add Pre-order Configuration".
3. In the drawer, select the available date for the pre-order.
4. Click the "Save" button.

The widget will be updated to show the pre-order configuration details.

![Pre-order variant widget showing the pre-order configuration](https://res.cloudinary.com/dza7lstvk/image/upload/v1752735733/Medusa%20Resources/CleanShot_2025-07-17_at_10.01.03_2x_bdvzo9.png)

You can also disable the pre-order configuration by clicking on the <InlineIcon Icon={EllipsisHorizontal} alt="three-dots" /> icon, then choosing "Remove Pre-order Configuration" from the dropdown.

***

## Step 6: Customize Cart Completion

When customers purchase a pre-order variant, you want to create a `Preorder` record for every pre-order item in the cart.

In this step, you'll wrap custom logic around Medusa's cart completion logic in a workflow, then execute that workflow in a custom API route.

### a. Create Complete Pre-order Cart Workflow

The workflow that completes a cart with pre-order items has the following steps:

- [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md): Acquire a lock on the cart to prevent concurrent modifications.
- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md): Complete the cart with pre-order items.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve existing preorders of the order for idempotency.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve all line items in the cart.
- [retrievePreorderItemIdsStep](#retrievePreorderItemIdsStep): Retrieve the IDs of pre-order variants in the cart.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the created order.
- [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md): Release the lock on the cart.

You only need to implement the `retrievePreorderItemIdsStep` and `createPreordersStep` steps.

#### retrievePreorderItemIdsStep

The `retrievePreorderItemIdsStep` receives all cart line items and returns the IDs of the pre-order variants.

Create the file `src/workflows/steps/retrieve-preorder-items.ts` with the following content:

```ts title="src/workflows/steps/retrieve-preorder-items.ts"
import { CartLineItemDTO, ProductVariantDTO } from "@medusajs/framework/types"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

export type RetrievePreorderItemIdsStepInput = {
  line_items: (CartLineItemDTO & {
    variant: ProductVariantDTO & {
      preorder_variant?: {
        id: string
      }
    }
  })[]
}

export const retrievePreorderItemIdsStep = createStep(
  "retrieve-preorder-item-ids",
  async ({ line_items }: RetrievePreorderItemIdsStepInput) => {
    const variantIds: string[] = []

    line_items.forEach((item) => {
      if (item.variant.preorder_variant) {
        variantIds.push(item.variant.preorder_variant.id)
      }
    })

    return new StepResponse(variantIds)
  }
)
```

In the step, you find the items in the cart whose variants have a linked `PreorderVariant` record. You return the IDs of those pre-order variants.

#### createPreordersStep

The `createPreordersStep` creates a `Preorder` record for each pre-order variant in the cart.

Create the file `src/workflows/steps/create-preorders.ts` with the following content:

```ts title="src/workflows/steps/create-preorders.ts" highlights={createPreordersStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PREORDER_MODULE } from "../../modules/preorder"

type StepInput = {
  preorder_variant_ids: string[]
  order_id: string
}

export const createPreordersStep = createStep(
  "create-preorders",
  async ({
    preorder_variant_ids,
    order_id,
  }: StepInput, { container }) => {
    const preorderModuleService = container.resolve(PREORDER_MODULE)

    const preorders = await preorderModuleService.createPreorders(
      preorder_variant_ids.map((id) => ({
        item_id: id,
        order_id,
      }))
    )

    return new StepResponse(preorders, preorders.map((p) => p.id))
  },
  async (preorderIds, { container }) => {
    if (!preorderIds) {
      return
    }

    const preorderModuleService = container.resolve(PREORDER_MODULE)

    await preorderModuleService.deletePreorders(preorderIds)
  }
)
```

The step function receives the ID of the Medusa order and the IDs of the pre-order variants in the cart.

In the step function, you create a `Preorder` record for each pre-order variant. You set the `order_id` of the `Preorder` record to the ID of the Medusa order.

In the compensation function, you delete the created `Preorder` records if an error occurs in the workflow's execution.

#### Create Workflow

You can now create the workflow that completes a cart with pre-order items.

Create the file `src/workflows/complete-cart-preorder.ts` with the following content:

```ts title="src/workflows/complete-cart-preorder.ts" highlights={completeCartPreorderWorkflowHighlights} collapsibleLines="1-17" expandButtonLabel="Show Imports"
import { 
  createWorkflow, 
  when, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  acquireLockStep, 
  completeCartWorkflow, 
  useQueryGraphStep, 
  releaseLockStep,
} from "@medusajs/medusa/core-flows"
import { 
  retrievePreorderItemIdsStep, 
  RetrievePreorderItemIdsStepInput,
} from "./steps/retrieve-preorder-items"
import { createPreordersStep } from "./steps/create-preorders"

type WorkflowInput = {
  cart_id: string
}

export const completeCartPreorderWorkflow = createWorkflow(
  "complete-cart-preorder",
  (input: WorkflowInput) => {
    acquireLockStep({
      key: input.cart_id,
      timeout: 2,
      ttl: 10,
    })
    const { id } = completeCartWorkflow.runAsStep({
      input: {
        id: input.cart_id,
      },
    })

    const { data: preorders } = useQueryGraphStep({
      entity: "preorder",
      fields: [
        "id",
      ],
      filters: {
        order_id: id,
      },
    })

    const { data: line_items } = useQueryGraphStep({
      entity: "line_item",
      fields: [
        "variant.*",
        "variant.preorder_variant.*",
      ],
      filters: {
        cart_id: input.cart_id,
      },
    }).config({ name: "retrieve-line-items" })

    const preorderItemIds = retrievePreorderItemIdsStep({
      line_items,
    } as unknown as RetrievePreorderItemIdsStepInput)

    when({
      preorders,
      preorderItemIds,
    }, (data) => data.preorders.length === 0 && data.preorderItemIds.length > 0)
    .then(() => {
      createPreordersStep({
        preorder_variant_ids: preorderItemIds,
        order_id: id,
      })
    })

    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "*",
        "items.*",
        "items.variant.*",
        "items.variant.preorder_variant.*",
        "shipping_address.*",
        "billing_address.*",
        "payment_collections.*",
        "shipping_methods.*",
      ],
      filters: {
        id,
      },
    }).config({ name: "retrieve-order" })

    releaseLockStep({
      key: input.cart_id,
    })
    
    return new WorkflowResponse({
      order: orders[0],
      
    })
  }
)
```

The workflow receives the cart ID as input.

In the workflow, you:

1. Acquire a lock on the cart using the [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md) to prevent concurrent modifications.
2. Complete the cart using the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) as a step. This is Medusa's cart completion logic.
3. Retrieve existing pre-orders of the created order using the [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md).
   - This is essential for idempotency, ensuring that pre-orders are not created multiple times if the workflow is retried.
4. Retrieve all line items in the cart using the [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md).
5. Retrieve the IDs of the pre-order variants in the cart using the `retrievePreorderItemIdsStep`.
6. Use [when-then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) to check if there are no existing pre-orders and if there are pre-order items in the cart.
   - If the condition is met, you create `Preorder` records for the pre-order items using the `createPreordersStep`.
7. Retrieve the created order using the [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md).
8. Release the lock on the cart using the [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md).
9. Return the created Medusa order.

### b. Create Complete Pre-order Cart API Route

Next, you'll create an API route that executes the `completeCartPreorderWorkflow`. Storefronts will use this API route to complete carts and place orders.

Create the file `src/api/store/carts/[id]/complete-preorder/route.ts` with the following content:

```ts title="src/api/store/carts/[id]/complete-preorder/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { completeCartPreorderWorkflow } from "../../../../../workflows/complete-cart-preorder"

export const POST = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const { id } = req.params

  const { result } = await completeCartPreorderWorkflow(req.scope).run({
    input: {
      cart_id: id,
    },
  })

  res.json({
    type: "order",
    order: result.order,
  })
}
```

You expose a `POST` API route at `/store/carts/:id/complete-preorder`. In the API route, you execute the `completeCartPreorderWorkflow`, passing it the cart ID from the path parameter.

Finally, you return the created order in the response.

You'll test this functionality in the next step when you customize the storefront.

***

## Optional: Restrict Cart to Pre-order or Regular Items

In some use cases, you may want to allow customers to either pre-order products, or order available ones, but not purchase both within the same order.

In those cases, you can perform custom logic within Medusa's add-to-cart logic to validate an item before it's added to the cart.

You do this using [workflow hooks](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md). A workflow hook is a point in a workflow where you can inject custom functionality as a step function.

To consume the `validate` hook of the `addToCartWorkflow` that holds the add-to-cart logic, create the file `src/workflows/hooks/validate-cart.ts` with the following content:

```ts title="src/workflows/hooks/validate-cart.ts" highlights={validateCartHookHighlights}
import { MedusaError } from "@medusajs/framework/utils"
import { addToCartWorkflow } from "@medusajs/medusa/core-flows"
import { InferTypeOf } from "@medusajs/framework/types"
import { PreorderVariant } from "../../modules/preorder/models/preorder-variant"

function isPreorderVariant(
  preorderVariant: InferTypeOf<typeof PreorderVariant> | undefined
) {
  if (!preorderVariant) {
    return false
  }
  return preorderVariant.status === "enabled" && 
    preorderVariant.available_date > new Date()
}

addToCartWorkflow.hooks.validate(
  (async ({ input, cart }, { container }) => {
    const query = container.resolve("query")

    const { data: itemsInCart } = await query.graph({
      entity: "line_item",
      fields: ["variant.*", "variant.preorder_variant.*"],
      filters: {
        cart_id: cart.id,
      },
    })

    if (!itemsInCart.length) {
      return
    }
    
    const { data: variantsToAdd } = await query.graph({
      entity: "variant",
      fields: ["preorder_variant.*"],
      filters: {
        id: input.items
          .map((item) => item.variant_id)
          .filter(Boolean) as string[],
      },
    })

    const cartHasPreorderVariants = itemsInCart.some(
      (item) => isPreorderVariant(
        item.variant?.preorder_variant as InferTypeOf<typeof PreorderVariant>
      )
    )

    const newItemsHavePreorderVariants = variantsToAdd.some(
      (variant) => isPreorderVariant(
        variant.preorder_variant as InferTypeOf<typeof PreorderVariant>
      )
    )

    if (cartHasPreorderVariants !== newItemsHavePreorderVariants) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "The cart must either contain only preorder variants, or available variants."
      )
    }
  })
)
```

You consume the hook by calling `addToCartWorkflow.hooks.validate`, passing it a step function.

In the step function, you check whether the cart's existing items have pre-order variants, and whether the new items have pre-order variants.

If the checks don't match, you throw an error, preventing the new item from being added to the cart.

Refer to the [Workflow Hooks](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md) documentation to learn more.

You can test this out after customizing the storefront in the next section.

***

## Step 7: Customize Storefront for Pre-orders

In this step, you'll customize the Next.js Starter Storefront to show when products are pre-orderable, use the custom complete pre-order cart API route, and show pre-order information in the order confirmation page.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-preorder`, you can find the storefront by going back to the parent directory and changing to the `medusa-preorder-storefront` directory:

```bash
cd ../medusa-preorder-storefront # change based on your project name
```

### a. Add Pre-order Types

You'll start by defining types related to pre-orders that you'll use in the storefront.

In `src/types/global.ts`, add the following import at the top of the file:

```ts title="src/types/global.ts" badgeLabel="Storefront" badgeColor="blue"
import { StorePrice } from "@medusajs/types"
```

Then, add the following type definitions at the end of the file:

```ts title="src/types/global.ts" highlights={globalTypesHighlights} badgeLabel="Storefront" badgeColor="blue"
export type StorePreorderVariant = {
  id: string
  variant_id: string
  available_date: string
  status: "enabled" | "disabled"
}

export type StoreProductVariantWithPreorder = HttpTypes.StoreProductVariant & {
  preorder_variant?: StorePreorderVariant
}

export type StoreCartLineItemWithPreorder = HttpTypes.StoreCartLineItem & {
  variant: StoreProductVariantWithPreorder
}
```

You'll use these type definitions in other customizations.

### b. Add isPreorder Utility

In product, cart, and order related pages, you'll need to check if a product variant is a pre-order variant. So, you'll create a utility function that you can reuse in those pages.

Create the file `src/lib/util/is-preorder.ts` with the following content:

```ts title="src/lib/util/is-preorder.ts" badgeLabel="Storefront" badgeColor="blue"
import { StorePreorderVariant } from "../../types/global"

export function isPreorder(
  preorderVariant: StorePreorderVariant | undefined
): boolean {
  return preorderVariant?.status === "enabled" &&
    (preorderVariant.available_date
      ? new Date(preorderVariant.available_date) > new Date()
      : false)
}
```

The function returns `true` if the pre-order variant has a `status` of `enabled` and an `available_date` in the future.

### c. Show Pre-order Information in Product Page

In this section, you'll customize the product page to show pre-order information when a customer selects a pre-order variant.

#### Retrieve Pre-order Variant Information

First, you'll retrieve the pre-order variant information along with the product variant from the Medusa server.

Since the `PreorderVariant` model is linked to the `ProductVariant` model, you can retrieve pre-order variants when retrieving product variants using the `fields` query parameter.

In the file `src/lib/data/products.ts`, find the `listProducts` function and update the `fields` property in the `sdk.client.fetch` method to include the pre-order variant:

```ts title="src/lib/data/products.ts" highlights={[["17"]]} badgeLabel="Storefront" badgeColor="blue"
export const listProducts = async ({
  // ...
}: {
  // ...
}): Promise<{
  // ...
}> => {
  // ...
  return sdk.client
    .fetch<{ products: HttpTypes.StoreProduct[]; count: number }>(
      `/store/products`,
      {
        method: "GET",
        query: {
          // ...
          fields:
            "*variants.calculated_price,+variants.inventory_quantity,+metadata,+tags,*variants.preorder_variant",
          // ...
        },
        // ...
      }
    )
  // ...
}
```

You add `*variants.preorder_variant` at the end of the `fields` query parameter to retrieve the pre-order variant information along with the product variants.

#### Customize ProductActions Component

Next, you'll customize the `ProductActions` component to show the pre-order information when a pre-order variant is selected.

In `src/modules/products/components/product-actions/index.tsx`, add the following imports at the top of the file:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { Text } from "@medusajs/ui"
import { StoreProductVariantWithPreorder } from "../../../../types/global"
import { isPreorder } from "../../../../lib/util/is-preorder"
```

Then, in the `ProductActions` component, add the following variable before the return statement:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const isSelectedVariantPreorder = useMemo(() => {
  return isPreorder(
    (selectedVariant as StoreProductVariantWithPreorder)?.preorder_variant
  )
}, [selectedVariant])
```

The `isSelectedVariantPreorder` variable is a boolean that indicates whether the selected product variant is a pre-order variant.

Next, find the `Button` component in the `return` statement and update its children to the following:

```tsx title="src/modules/products/components/product-actions/index.tsx" highlights={[["11"]]} badgeLabel="Storefront" badgeColor="blue"
return (
  <>
    {/* ... other components ... */}
    <Button
      // ...
    >
      {!selectedVariant && !options
        ? "Select variant"
        : !inStock || !isValidVariant
        ? "Out of stock"
        : isSelectedVariantPreorder ? "Pre-order" : "Add to cart"}
    </Button>
    {/* ... other components ... */}
  </>
)
```

You set the button text to "Pre-order" if the selected variant is a pre-order variant.

Finally, add the following code after the `Button` component in the `return` statement:

```tsx title="src/modules/products/components/product-actions/index.tsx" highlights={[["9"], ["10"], ["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} badgeLabel="Storefront" badgeColor="blue"
return (
  <>
    {/* ... other components ... */}
    <Button
      // ...
    >
      {/* ... */}
    </Button>
    {isSelectedVariantPreorder && (
      <Text className="text-ui-fg-muted text-xs text-center">
        This item will be shipped on{" "}
        {new Date(
          (selectedVariant as StoreProductVariantWithPreorder)!.preorder_variant!.available_date
        ).toLocaleDateString()}.
      </Text>
    )}
    {/* ... other components ... */}
  </>
)
```

You show a message below the button that indicates when the pre-order item will be shipped.

#### Test the Product Page Customizations

To test out the changes to the product page, start the Medusa application with the following command:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

And in the Next.js Starter Storefront directory, start the Next.js application with the following command:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

Open the storefront at `http://localhost:8000` and go to Menu -> Store.

Choose a product that has a pre-order variant, then select the pre-order variant's options. The button text will change to "Pre-order" and a message will appear below the button indicating when the item will be shipped.

![Pre-order product variant in the Next.js Starter Storefront](https://res.cloudinary.com/dza7lstvk/image/upload/v1752739520/Medusa%20Resources/CleanShot_2025-07-17_at_11.04.56_2x_fefjo6.png)

You can click on the "Pre-order" button to add the item to the cart.

If you consumed the `validate` hook of the `addToCartWorkflow` as explained in the [optional step](#optional-restrict-cart-to-pre-order-or-regular-items), you can test it out now by trying to add a regular item to the cart.

### d. Show Pre-order Information in Cart Page

Next, you'll customize the component showing items in the cart and checkout pages to show pre-order information.

#### Retrieve Pre-order Information in Cart

To show the pre-order information of items in the cart, you need to retrieve the pre-order variant information when retrieving the cart.

In the file `src/lib/data/cart.ts`, find the `retrieveCart` function and update the `fields` property in the `sdk.client.fetch` method to include the pre-order variant:

```ts title="src/lib/data/cart.ts" highlights={[["8"]]} badgeLabel="Storefront" badgeColor="blue"
export async function retrieveCart(cartId?: string) {
  // ...
  return await sdk.client
    .fetch<HttpTypes.StoreCartResponse>(`/store/carts/${id}`, {
      method: "GET",
      query: {
        fields:
          "*items, *region, *items.product, *items.variant, *items.thumbnail, *items.metadata, +items.total, *promotions, +shipping_methods.name, *items.variant.preorder_variant",
      },
      // ...
    })
  // ...
}
```

Notice that you added `*items.variant.preorder_variant` at the end of the retrieved fields.

#### Customize Cart Item Component

Next, you'll customize the `Item` component that shows each item in the cart and checkout pages to show pre-order information.

In `src/modules/cart/components/item/index.tsx`, add the following imports at the top of the file:

```tsx title="src/modules/cart/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { StoreCartLineItemWithPreorder } from "../../../../types/global"
import { isPreorder } from "../../../../lib/util/is-preorder"
```

Next, change the type of the `item` prop in `ItemProps`:

```tsx title="src/modules/cart/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue"
type ItemProps = {
  item: StoreCartLineItemWithPreorder
  // ...
}
```

Then, in the `Item` component, add the following variable before the return statement:

```tsx title="src/modules/cart/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const isPreorderItem = isPreorder(item.variant?.preorder_variant)
```

Finally, in the `return` statement, add the following after the `LineItemOptions` component:

```tsx title="src/modules/cart/components/item/index.tsx" highlights={[["7"], ["8"], ["9"], ["10"], ["11"], ["12"], ["13"], ["14"]]} badgeLabel="Storefront" badgeColor="blue"
return (
  <Table.Row className="w-full" data-testid="product-row">
    {/* ... other components ... */}
    <LineItemOptions
      // ...
    />
    {isPreorderItem && (
      <Text className="text-ui-fg-muted text-xs">
        Preorder available on{" "}
        <span>
          {new Date(item.variant!.preorder_variant!.available_date).toLocaleDateString()}
        </span>
      </Text>
    )}
    {/* ... other components ... */}
  </Table.Row>
)
```

You show a message below the line item options that indicates when the pre-order item will be available.

The change in the `ItemProps` type will cause a type error in `src/modules/cart/templates/items.tsx` that uses this type.

To fix it, add in `src/modules/cart/templates/items.tsx` the following import at the top of the file:

```tsx title="src/modules/cart/templates/items.tsx" badgeLabel="Storefront" badgeColor="blue"
import { StoreCartLineItemWithPreorder } from "../../../types/global"
```

Then, in the `return` statement of the `ItemsTemplate` component, find the `Item` component and change its `item` prop:

```tsx title="src/modules/cart/templates/items.tsx" highlights={[["5"]]} badgeLabel="Storefront" badgeColor="blue"
return (
  <div>
    {/* ... other components ... */}
    <Item
      item={item as StoreCartLineItemWithPreorder}
      // ...
    />
    {/* ... other components ... */}
  </div>
)
```

#### Test the Cart Page Customizations

To test the changes to the cart page, ensure that both the Medusa and Next.js applications are running.

Then, in the storefront, click on the "Cart" link at the top right of the page. You'll find that pre-order items have a message indicating when they're available.

![Pre-order item in the cart page of the Next.js Starter Storefront](https://res.cloudinary.com/dza7lstvk/image/upload/v1752740244/Medusa%20Resources/CleanShot_2025-07-17_at_11.17.04_2x_u4opzf.png)

You can also see this message on the checkout page.

### e. Use Custom Complete Pre-order Cart API Route

Next, you'll use the custom API route you created to complete carts. This will allow you to create `Preorder` records for pre-order items in the cart when the customer places an order.

In `src/lib/data/cart.ts`, find the following lines in the `placeOrder` function:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
export async function placeOrder(cartId?: string) {
  // ...
  const cartRes = await sdk.store.cart
  .complete(id, {}, headers)
  // ...
}
```

And replace them with the following:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
export async function placeOrder(cartId?: string) {
  // ...
  const cartRes = await sdk.client.fetch<HttpTypes.StoreCompleteCartResponse>(
    `/store/carts/${id}/complete-preorder`, 
    {
      headers,
      method: "POST",
    }
  )
  // ...
}
```

You change the `placeOrder` function, which is executed when the customer places an order, to use the custom API route you created to complete carts with pre-order items.

#### Test Cart Completion

To test the cart completion with pre-order items, ensure that both the Medusa and Next.js applications are running.

Then, in the storefront, proceed to checkout with a pre-order item in the cart.

When you place the order, the custom API route will be executed and the order will be placed.

### f. Show Pre-order Information in Order Confirmation Page

Finally, you'll show the pre-order information in the order confirmation and detail pages.

#### Retrieve Pre-order Information in Order

To show the pre-order information in the order confirmation and detail pages, you need to retrieve the pre-order variant information when retrieving the order.

In the file `src/lib/data/orders.ts`, find the `retrieveOrder` function and update the `fields` property in the `sdk.client.fetch` method to include the pre-order variant:

```ts title="src/lib/data/orders.ts" highlights={[["8"]]} badgeLabel="Storefront" badgeColor="blue"
export const retrieveOrder = async (id: string) => {
  // ...
  return sdk.client
    .fetch<HttpTypes.StoreOrderResponse>(`/store/orders/${id}`, {
      method: "GET",
      query: {
        fields:
          "*payment_collections.payments,*items,*items.metadata,*items.variant,*items.product,*items.variant.preorder_variant",
      },
      // ...
    })
  // ...
}
```

Notice that you added `*items.variant.preorder_variant` at the end of the retrieved fields.

#### Customize Order Item Component

Next, you'll customize the `Item` component that renders each item in the order confirmation and detail pages to show pre-order information.

In `src/modules/order/components/item/index.tsx`, add the following imports at the top of the file:

```tsx title="src/modules/order/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { StoreProductVariantWithPreorder } from "../../../../types/global"
import { isPreorder } from "../../../../lib/util/is-preorder"
```

Then, change the type of the `item` prop in `ItemProps`:

```tsx title="src/modules/order/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue"
type ItemProps = {
  item: (HttpTypes.StoreCartLineItem | HttpTypes.StoreOrderLineItem) & {
    variant?: StoreProductVariantWithPreorder
  }
  // ...
}
```

Next, in the `Item` component, add the following variable:

```tsx title="src/modules/order/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const isPreorderItem = isPreorder(item.variant?.preorder_variant)
```

Finally, in the `return` statement, add the following after the `LineItemOptions` component:

```tsx title="src/modules/order/components/item/index.tsx" highlights={[["7"], ["8"], ["9"], ["10"], ["11"], ["12"], ["13"], ["14"]]} badgeLabel="Storefront" badgeColor="blue"
return (
  <Table.Row className="w-full" data-testid="product-row">
    {/* ... other components ... */}
    <LineItemOptions
      // ...
    />
    {isPreorderItem && (
      <Text className="text-ui-fg-muted text-xs">
        Preorder available on{" "}
        <span>
          {new Date(item.variant!.preorder_variant!.available_date).toLocaleDateString()}
        </span>
      </Text>
    )}
    {/* ... other components ... */}
  </Table.Row>
)
```

#### Test the Order Confirmation Page Customizations

To test the changes to the order confirmation page, ensure that both the Medusa and Next.js applications are running.

Then, in the storefront, either open the same confirmation page you got earlier after placing the order, or place a new order with a pre-order item in the cart.

You'll find the pre-order information below the product variant options in the order confirmation page.

![Pre-order item in the order confirmation page of the Next.js Starter Storefront](https://res.cloudinary.com/dza7lstvk/image/upload/v1752741220/Medusa%20Resources/CleanShot_2025-07-17_at_11.33.17_2x_anitlq.png)

The information will also be shown in the order details page for logged-in customers.

***

## Step 8: Show Pre-Order Information in Order Admin Page

In this step, you'll customize the Medusa Admin to show pre-order information in the order details page.

### a. Retrieve Pre-order Information API Route

You'll start by creating an API route that retrieves the pre-order information for an order. You'll send requests to this API route in the widget you'll create in the next step.

Create the file `src/api/admin/orders/[id]/preorders/route.ts` with the following content:

```ts title="src/api/admin/orders/[id]/preorders/route.ts"
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve("query")
  const { id: orderId } = req.params

  const { data: preorders } = await query.graph({
    entity: "preorder",
    fields: [
      "*", 
      "item.*", 
      "item.product_variant.*", 
      "item.product_variant.product.*",
    ],
    filters: {
      order_id: orderId,
    },
  })

  res.json({ preorders })
}
```

You expose a `GET` API route at `/admin/orders/:id/preorders`.

In the API route, you use Query to retrieve the pre-order information for the specified order ID. You return the pre-order information in the response.

### b. Add Pre-order React Hook

Next, you'll add a React hook that retrieves the pre-order information from the API route you created.

Create the file `src/admin/hooks/use-preorders.ts` with the following content:

```ts title="src/admin/hooks/use-preorders.ts" highlights={usePreordersHighlights}
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"
import { Preorder, PreordersResponse } from "../lib/types"

export const usePreorders = (orderId: string) => {
  const { data, isLoading, error } = useQuery<PreordersResponse>({
    queryFn: () => sdk.client.fetch(`/admin/orders/${orderId}/preorders`),
    queryKey: ["orders", orderId],
    retry: 2,
    refetchOnWindowFocus: false,
  })

  return {
    preorders: data?.preorders || [],
    isLoading,
    error,
  }
}

export type { Preorder }
```

The `usePreorders` hook retrieves the pre-order information for the specified order ID using the API route you created.

### c. Create Pre-order Widget

Finally, you'll create a widget that shows the pre-order information in the order details page.

Create the file `src/admin/widgets/preorder-widget.tsx` with the following content:

```tsx title="src/admin/widgets/preorder-widget.tsx" highlights={preorderWidgetHighlights}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types"
import { Container, Heading, StatusBadge, Text } from "@medusajs/ui"
import { Link } from "react-router-dom"
import { usePreorders } from "../hooks/use-preorders"

const PreordersWidget = ({
  data: order,
}: DetailWidgetProps<HttpTypes.AdminOrder>) => {
  const { preorders, isLoading } = usePreorders(order.id)

  if (!preorders.length && !isLoading) {
    return <></>
  }

  return (
    <Container className="divide-y p-0">
      <div className="flex flex-col justify-between py-4">
        <div className="flex flex-col gap-2 px-6">
          <Heading level="h2">
            Pre-orders
          </Heading>
          <Text className="text-ui-fg-muted" size="small">
            The following items will be automatically fulfilled on their available date.
          </Text>
        </div>
        {isLoading && <div>Loading...</div>}
        <div className="flex flex-col gap-4 pt-4 px-6">
          {preorders.map((preorder) => (
            <div key={preorder.id} className="flex items-center gap-2">
              {preorder.item.product_variant?.product?.thumbnail && <img 
                src={preorder.item.product_variant.product.thumbnail} 
                alt={preorder.item.product_variant.title || "Product Thumbnail"} 
                className="w-20 h-20 rounded-lg border"
              />}
              <div className="flex flex-col gap-1">
                <div className="flex gap-2">
                  <Text>{preorder.item.product_variant?.title || "Unnamed Variant"}</Text>
                  <StatusBadge color={getStatusBadgeColor(preorder.status)}>
                    {preorder.status.charAt(0).toUpperCase() + preorder.status.slice(1)}
                  </StatusBadge>
                </div>
                <Text size="small" className="text-ui-fg-subtle">
                  Available on: {new Date(preorder.item.available_date).toLocaleDateString()}
                </Text>
                <Link to={`/products/${preorder.item.product_variant?.product_id}/variants/${preorder.item.variant_id}`}>
                  <Text size="small" className="text-ui-fg-interactive">
                    View Product Variant
                  </Text>
                </Link>
              </div>
            </div>
          ))}
        </div>
      </div>
    </Container>
  )
}

const getStatusBadgeColor = (status: string) => {
  switch (status) {
    case "fulfilled":
      return "green"
    case "pending":
      return "orange"
    default:
      return "grey"
  }
}

export const config = defineWidgetConfig({
  zone: "order.details.side.after",
})

export default PreordersWidget
```

The `PreordersWidget` will be injected into the side column of the order details page in the Medusa Admin dashboard.

In the component, you retrieve the pre-order information using the `usePreorders` hook and display it in a list.

### Test the Pre-order Widget

To test the pre-order widget, ensure that the Medusa application is running.

Then, in the Medusa Admin dashboard, go to any order that has pre-order items. You should see the "Pre-orders" section in the side column with the pre-order information.

![Pre-orders Section in the Medusa Admin dashboard](https://res.cloudinary.com/dza7lstvk/image/upload/v1752741736/Medusa%20Resources/CleanShot_2025-07-17_at_11.41.55_2x_w8aiw2.png)

You can see the pre-order variant's title, status, available date, and a link to view the associated product variant.

***

## Step 9: Automatically Fulfill Pre-orders

When a pre-order variant reaches its available date, you want to automatically fulfill the pre-order items in the order.

In this step, you'll create a workflow that automatically fulfills a pre-order. Then, you'll execute the workflow in a [scheduled job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md) that runs every day.

### a. Create Auto Fulfill Pre-order Workflow

The workflow that automatically fulfills a pre-order has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the Medusa order details.
- [retrieveItemsToFulfillStep](#retrieveItemsToFulfillStep): Retrieve the items to fulfill in the order.
- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md): Create a fulfillment for the Medusa order.
- [updatePreordersStep](#updatePreordersStep): Update the status of the pre-order.
- [emitEventStep](https://docs.medusajs.com/references/helper-steps/emitEventStep/index.html.md): Emit an event indicating that a pre-order was fulfilled.

You only need to implement the `retrieveItemsToFulfillStep` and `updatePreordersStep` steps.

#### retrieveItemsToFulfillStep

The `retrieveItemsToFulfillStep` retrieves the line items to fulfill in an order based on the supplied pre-order variants.

Create the file `src/workflows/steps/retrieve-items-to-fulfill.ts` with the following content:

```ts title="src/workflows/steps/retrieve-items-to-fulfill.ts" highlights={retrieveItemsToFulfillHighlights}
import { InferTypeOf, OrderLineItemDTO } from "@medusajs/framework/types"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PreorderVariant } from "../../modules/preorder/models/preorder-variant"

export type RetrieveItemsToFulfillStepInput = {
  preorder_variant: InferTypeOf<typeof PreorderVariant>
  line_items: OrderLineItemDTO[]
}

export const retrieveItemsToFulfillStep = createStep(
  "retrieve-items-to-fulfill",
  async ({
    preorder_variant,
    line_items,
  }: RetrieveItemsToFulfillStepInput) => {
    const itemsToFulfill = line_items.filter((item) =>
      item.variant_id && preorder_variant.variant_id === item.variant_id
    ).map((item) => ({
      id: item.id,
      quantity: item.quantity,
    }))

    return new StepResponse({
      items_to_fulfill: itemsToFulfill,
    })
  }
)
```

The step receives the pre-order variant to be fulfilled as an input, and the line items in the Medusa order.

In the step, you find the items in the Medusa order that are associated with the pre-order variant and return them.

#### updatePreordersStep

The `updatePreordersStep` updates the details of pre-order records.

Create the file `src/workflows/steps/update-preorders.ts` with the following content:

```ts title="src/workflows/steps/update-preorders.ts" highlights={updatePreordersHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PreorderStatus } from "../../modules/preorder/models/preorder"
import { PREORDER_MODULE } from "../../modules/preorder"

type StepInput = {
  id: string
  status?: PreorderStatus
  item_id?: string
  order_id?: string
}[]

export const updatePreordersStep = createStep(
  "update-preorders",
  async (preorders: StepInput, { container }) => {
    const preorderModuleService = container.resolve(PREORDER_MODULE)

    const oldPreorders = await preorderModuleService.listPreorders({
      id: preorders.map((preorder) => preorder.id),
    })

    const updatedPreorders = await preorderModuleService.updatePreorders(
      preorders
    )

    return new StepResponse(updatedPreorders, oldPreorders)
  },
  async (preorders, { container }) => {
    if (!preorders) {
      return
    }

    const preorderModuleService = container.resolve(PREORDER_MODULE)

    await preorderModuleService.updatePreorders(
      preorders.map((preorder) => ({
        id: preorder.id,
        status: preorder.status,
        item_id: preorder.item_id,
        order_id: preorder.order_id,
      }))
    )
  }
)
```

The step receives an array of pre-order records to update.

In the step, you update the pre-order records. In the compensation function, you undo the update if an error occurs during the workflow's execution.

#### Create Workflow

Finally, you'll create the workflow that automatically fulfills a pre-order.

Create the file `src/workflows/fulfill-preorder.ts` with the following content:

```ts title="src/workflows/fulfill-preorder.ts" highlights={fulfillPreorderWorkflowHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports"
import { InferTypeOf } from "@medusajs/framework/types"
import { PreorderVariant } from "../modules/preorder/models/preorder-variant"
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { createOrderFulfillmentWorkflow, emitEventStep, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { retrieveItemsToFulfillStep, RetrieveItemsToFulfillStepInput } from "./steps/retrieve-items-to-fulfill"
import { updatePreordersStep } from "./steps/update-preorders"
import { PreorderStatus } from "../modules/preorder/models/preorder"

type WorkflowInput = {
  preorder_id: string
  item: InferTypeOf<typeof PreorderVariant>
  order_id: string
}

export const fulfillPreorderWorkflow = createWorkflow(
  "fulfill-preorder",
  (input: WorkflowInput) => {
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: ["items.*"],
      filters: {
        id: input.order_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const { items_to_fulfill } = retrieveItemsToFulfillStep({
      preorder_variant: input.item,
      line_items: orders[0].items,
    } as unknown as RetrieveItemsToFulfillStepInput)

    const fulfillment = createOrderFulfillmentWorkflow.runAsStep({
      input: {
        order_id: input.order_id,
        items: items_to_fulfill,
      },
    })

    updatePreordersStep([{
      id: input.preorder_id,
      status: PreorderStatus.FULFILLED,
    }])

    emitEventStep({
      eventName: "preorder.fulfilled",
      data: {
        order_id: input.order_id,
        preorder_variant_id: input.item.id,
      },
    })

    return new WorkflowResponse({
      fulfillment,
    })
  }
)
```

The workflow receives as an input:

- `preorder_id`: The ID of the pre-order to fulfill.
- `item`: The pre-order variant to fulfill.
- `order_id`: The ID of the Medusa order containing the pre-ordered variant.

In the workflow, you:

- Retrieve the Medusa order using the [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md).
- Retrieve the items to fulfill in the order using the `retrieveItemsToFulfillStep`.
- Create a fulfillment for the Medusa order using the [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) as a step.
- Update the pre-order status to `FULFILLED` using the `updatePreordersStep`.
- Emit an event indicating that a pre-order was fulfilled using the [emitEventStep](https://docs.medusajs.com/references/helper-steps/emitEventStep/index.html.md).
- Return the fulfillment in the response.

#### Optional: Capture Payment

By default, Medusa authorizes an order's payment when it's placed. The admin user can capture the payment later manually.

Some payment providers may automatically capture the payment when the order is placed, depending on their configuration.

In some use cases, you may want to capture the payment for the pre-order when fulfilling it.

To do that, you can use Medusa's [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md) to capture the payment for the order in the workflow.

First, change the `retrieveItemsToFulfillStep` to return the total of the pre-ordered items:

```ts title="src/workflows/steps/retrieve-items-to-fulfill.ts" highlights={[["16"], ["20"], ["29"]]}
import { InferTypeOf, OrderLineItemDTO } from "@medusajs/framework/types"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PreorderVariant } from "../../modules/preorder/models/preorder-variant"

export type RetrieveItemsToFulfillStepInput = {
  preorder_variant: InferTypeOf<typeof PreorderVariant>
  line_items: OrderLineItemDTO[]
}

export const retrieveItemsToFulfillStep = createStep(
  "retrieve-items-to-fulfill",
  async ({
    preorder_variant,
    line_items,
  }: RetrieveItemsToFulfillStepInput) => {
    let total = 0
    const itemsToFulfill = line_items.filter((item) =>
      item.variant_id && preorder_variant.variant_id === item.variant_id
    ).map((item) => {
      total += item.total as number
      return {
        id: item.id,
        quantity: item.quantity,
      }
    })

    return new StepResponse({
      items_to_fulfill: itemsToFulfill,
      items_total: total,
    })
  }
)
```

Then, update the workflow to the following:

```ts title="src/workflows/fulfill-preorder.ts"
import { InferTypeOf } from "@medusajs/framework/types"
import { PreorderVariant } from "../modules/preorder/models/preorder-variant"
import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { capturePaymentWorkflow, createOrderFulfillmentWorkflow, emitEventStep, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { retrieveItemsToFulfillStep, RetrieveItemsToFulfillStepInput } from "./steps/retrieve-items-to-fulfill"
import { updatePreordersStep } from "./steps/update-preorders"
import { PreorderStatus } from "../modules/preorder/models/preorder"

type WorkflowInput = {
  preorder_id: string
  item: InferTypeOf<typeof PreorderVariant>
  order_id: string
}

export const fulfillPreorderWorkflow = createWorkflow(
  "fulfill-preorder",
  (input: WorkflowInput) => {
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "items.*", 
        "payment_collections.*", 
        "payment_collections.payments.*", 
        "total", 
        "shipping_methods.*",
      ],
      filters: {
        id: input.order_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const { 
      items_to_fulfill, 
      items_total,
    } = retrieveItemsToFulfillStep({
      preorder_variant: input.item,
      line_items: orders[0].items,
    } as unknown as RetrieveItemsToFulfillStepInput)

    const fulfillment = createOrderFulfillmentWorkflow.runAsStep({
      input: {
        order_id: input.order_id,
        items: items_to_fulfill,
      },
    })

    updatePreordersStep([{
      id: input.preorder_id,
      status: PreorderStatus.FULFILLED,
    }])

    const totalCaptureAmount = transform({
      items_total,
      shipping_option_id: fulfillment.shipping_option_id,
      shipping_methods: orders[0].shipping_methods,
    }, (data) => {
      const shippingPrice = data.shipping_methods?.find(
        (sm) => sm?.shipping_option_id === data.shipping_option_id
      )?.amount || 0
      return data.items_total + shippingPrice
    })

    when({
      payment_collection: orders[0].payment_collections?.[0],
      capture_total: totalCaptureAmount,
    }, (data) => {
      return data.payment_collection?.amount !== undefined && data.payment_collection.captured_amount !== null &&  
        (data.payment_collection.amount - data.payment_collection.captured_amount >= data.capture_total)
    }).then(() => {
      capturePaymentWorkflow.runAsStep({
        input: {
          // @ts-ignore
          payment_id: orders[0].payment_collections?.[0]?.payments[0].id,
          amount: totalCaptureAmount,
        },
      })
    })

    emitEventStep({
      eventName: "preorder.fulfilled",
      data: {
        order_id: input.order_id,
        preorder_variant_id: input.item.id,
      },
    })

    return new WorkflowResponse({
      fulfillment,
    })
  }
)
```

The changes you made are:

- Retrieved more order fields using `useQueryGraphStep`, including the payment collection and its payments.
- Retrieved the total of the fulfilled items from `retrieveItemsToFulfillStep`.
- Calculated the amount to be captured by adding the fulfilled items' total to the shipping method's amount.
- If the total amount to be captured is less than or equal to the amount that can be captured, you capture the amount.

### b. Create Scheduled Job to Run Workflow

Next, you'll create a scheduled job that runs the `fulfillPreorderWorkflow` every day to automatically fulfill pre-orders.

A scheduled job is an asynchronous function that executes tasks in the background at specified intervals.

Refer to the [Scheduled Jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md) documentation to learn more.

To create a scheduled job, create the file `src/jobs/fulfill-preorders.ts` with the following content:

```ts title="src/jobs/fulfill-preorders.ts" highlights={fulfillPreordersJobHighlights}
import {
  InferTypeOf,
  MedusaContainer,
} from "@medusajs/framework/types"
import { fulfillPreorderWorkflow } from "../workflows/fulfill-preorder"
import { PreorderVariant } from "../modules/preorder/models/preorder-variant"

export default async function fulfillPreordersJob(container: MedusaContainer) {
  const query = container.resolve("query")
  const logger = container.resolve("logger")

  logger.info("Starting daily fulfill preorders job...")

  // TODO add fulfillment logic
}

export const config = {
  name: "daily-fulfill-preorders",
  schedule: "0 0 * * *", // Every day at midnight
}
```

A scheduled job file must export:

- An asynchronous function that is executed at the specified interval in the configuration object.
- A configuration object that specifies when to execute the scheduled job. The schedule is defined as a cron pattern.

So far, you only resolve Query and the Logger utility from the Medusa container passed as input to the scheduled job.

Replace the `TODO` in the scheduled job with the following:

```ts title="src/jobs/fulfill-preorders.ts" highlights={fulfillPreordersJobHighlights2}
const startToday = new Date()
startToday.setHours(0, 0, 0, 0)

const endToday = new Date()
endToday.setHours(23, 59, 59, 59)

const limit = 1000
let preorderVariantsOffset = 0
let preorderVariantsCount = 0
let totalPreordersCount = 0

do {
  const { 
    data: preorderVariants,
    metadata,
  } = await query.graph({
    entity: "preorder_variant",
    fields: [
      "*",
      "product_variant.*",
    ],
    filters: {
      status: "enabled",
      available_date: {
        $gte: startToday,
        $lte: endToday,
      },
    },
    pagination: {
      take: limit,
      skip: preorderVariantsOffset,
    },
  })

  preorderVariantsCount = metadata?.count || 0
  preorderVariantsOffset += limit

  let preordersOffset = 0
  let preordersCount = 0
  
  do {
    const { 
      data: unfulfilledPreorders,
      metadata: preorderMetadata,
    } = await query.graph({
      entity: "preorder",
      fields: ["*"],
      filters: {
        item_id: preorderVariants.map((variant) => variant.id),
        status: "pending",
      },
      pagination: {
        take: limit,
        skip: preordersOffset,
      },
    })
    if (!unfulfilledPreorders.length) {
      continue
    }

    preordersCount = preorderMetadata?.count || 0
    preordersOffset += limit
    for (const preorder of unfulfilledPreorders) {
      const variant = preorderVariants.find((v) => v.id === preorder.item_id)
      try {
        await fulfillPreorderWorkflow(container)
        .run({
          input: {
            preorder_id: preorder!.id,
            item: variant as unknown as InferTypeOf<typeof PreorderVariant>,
            order_id: preorder!.order_id,
          },
        })
      } catch (e) {
        logger.error(`Failed to fulfill preorder ${preorder.id}: ${e.message}`)
      }
    }
  } while (preordersCount > limit * preordersOffset)
  totalPreordersCount += preordersCount
} while (preorderVariantsCount > limit * preorderVariantsOffset)

logger.info(`Fulfilled ${totalPreordersCount} preorders.`)
```

You retrieve the pre-order variants whose:

- `status` is `enabled`.
- `available_date` is within the current day.

Then, you retrieve the pre-orders of those variants whose status is `pending`, and fulfill each of those pre-orders.

You apply pagination on both the retrieved pre-order variants and pre-orders.

Finally, you log the number of fulfilled pre-orders.

### Test Scheduled Job

To test out the scheduled job, you can change the `schedule` in the job's configuration to run once a minute:

```ts title="src/jobs/fulfill-preorders.ts"
export const config = {
  // ...
  schedule: "* * * * *", // Every minute for testing purposes
}
```

And comment-out the `available_date` condition in the first `query.graph` usage to retrieve all pre-order variants and fulfill their pre-orders:

```ts title="src/jobs/fulfill-preorders.ts" highlights={[["12"], ["13"], ["14"], ["15"]]}
const { 
  data: preorderVariants,
  metadata,
} = await query.graph({
  entity: "preorder_variant",
  fields: [
    "*",
    "product_variant.*",
  ],
  filters: {
    status: "enabled",
    // available_date: {
    //   $gte: startToday,
    //   $lte: endToday
    // }
  },
  pagination: {
    take: limit,
    skip: preorderVariantsOffset,
  },
})
```

Next, start the Medusa application:

```bash npm2yarn
npm run dev
```

After a minute, you'll see the following messages in the logs:

```bash
info:    Starting daily fulfill preorders job...
info:    Fulfilled 1 preorders.
```

This indicates that the scheduled job ran successfully and fulfilled one pre-order.

Make sure to revert the changes once you're done testing.

### Optional: Send Notification to Customer

In the `fulfillPreorderWorkflow`, you emitted the `preorder.fulfilled` event. This is useful for performing actions when a pre-order is fulfilled separately from the main flow.

For example, you may want to send a notification to the customer when their pre-order is fulfilled. You can do this by creating a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).

A subscriber is an asynchronous function that is executed when its associated event is emitted.

To create a subscriber that sends a notification to the customer when a pre-order is fulfilled, create the file `src/subscribers/preorder-notification.ts` with the following content:

```ts title="src/subscribers/preorder-notification.ts" highlights={preorderNotificationHighlights}
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"

export default async function productCreateHandler({
  event: { data },
  container,
}: SubscriberArgs<{ 
  order_id: string;
  preorder_variant_id: string;
 }>) {
  const query = container.resolve("query")
  const notificationModuleService = container.resolve(
    "notification"
  )

  const { data: preorderVariants } = await query.graph({
    entity: "preorder_variant",
    fields: ["*"],
    filters: {
      id: data.preorder_variant_id,
    },
  })

  const { data: [order] } = await query.graph({
    entity: "order",
    fields: ["*"],
    filters: {
      id: data.order_id,
    },
  })

  await notificationModuleService.createNotifications([{
    template: "preorder_fulfilled",
    channel: "feed",
    to: order.email!,
    data: {
      preorder_variant: preorderVariants[0],
      order: order,
    },
  }])
}

export const config: SubscriberConfig = {
  event: "preorder.fulfilled",
}
```

A subscriber file must export:

- An asynchronous function that is executed when its associated event is emitted.
- An object that indicates the event that the subscriber is listening to.

The subscriber receives among its parameters the data payload of the emitted event, which includes the IDs of the Medusa order and the pre-order variant.

In the subscriber, you retrieve the details of the order and pre-order variants. Then, you use the [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) to send a notification.

Notice that the `createNotifications` method receives a `channel` property for a notification. This indicates which [Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification#what-is-a-notification-module-provider/index.html.md) to send the notification with.

The `feed` channel is useful for debugging, as it logs the notification in the terminal. To send an email, you can instead [set up a provider like SendGrid](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/integrations#notification/index.html.md) and change the channel to `email`.

To test the subscriber, [perform the steps to test fulfilling pre-orders](#test-scheduled-job). Once a pre-order is fulfilled, you'll see the following message logged in your terminal:

```bash
info:    Processing preorder.fulfilled which has 1 subscribers
```

Learn more about sending notifications in the [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) documentation.

***

## Step 10: Handle Order Cancelation

Admin users can cancel orders from the dashboard. In those scenarios, you need to also cancel the pre-orders related to that order.

In this step, you'll create a workflow that cancels the pre-orders of an order. Then, you'll execute the workflow in a subscriber that listens to the order cancelation event.

### a. Cancel Pre-Orders Workflow

The workflow that cancels pre-orders of an order has the following steps:

- [updatePreordersStep](#updatePreordersStep): Update the statuses of the pre-orders to canceled.
- [emitEventStep](#emitEventStep): Emit an event for the pre-orders cancelation.

These steps are already available, so you can implement the workflow.

Create the file `src/workflows/cancel-preorders.ts` with the following content:

```ts title="src/workflows/cancel-preorders.ts" highlights={cancelPreordersWorkflowHighlights}
import { InferTypeOf } from "@medusajs/framework/types"
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { emitEventStep } from "@medusajs/medusa/core-flows"
import { updatePreordersStep } from "./steps/update-preorders"
import { Preorder, PreorderStatus } from "../modules/preorder/models/preorder"

export type CancelPreordersWorkflowInput = {
  preorders: InferTypeOf<typeof Preorder>[]
  order_id: string
}

export const cancelPreordersWorkflow = createWorkflow(
  "cancel-preorders",
  (input: CancelPreordersWorkflowInput) => {
    const updateData = transform({
      preorders: input.preorders,
    }, (data) => {
      return data.preorders.map((preorder) => ({
        id: preorder.id,
        status: PreorderStatus.CANCELLED,
      }))
    })

    const updatedPreorders = updatePreordersStep(updateData)

    const preordersCancelledEvent = transform({
      preorders: updatedPreorders,
      input,
    }, (data) => {
      return data.preorders.map((preorder) => ({
        id: preorder.id,
        order_id: data.input.order_id,
      }))
    })

    emitEventStep({
      eventName: "preorder.cancelled",
      data: preordersCancelledEvent,
    })

    return new WorkflowResponse({
      preorders: updatedPreorders,
    })
  }
)
```

The workflow receives the preorders and the order ID as a parameter.

In the workflow, you:

1. Update the pre-orders' status to `cancelled`.
2. Emit the `preorder.cancelled` event.
   - You can handle the event similar to the [preorder.fulfilled](#optional-send-notification-to-customer) event to notify the customer.

### b. Cancel Pre-Orders Subscriber

Next, you'll create the subscriber that listens to the `order.canceled` event and executes the workflow you created.

Create the file `src/subscribers/order-canceled.ts` with the following content:

```ts title="src/subscribers/order-canceled.ts" highlights={orderCanceledHighlights}
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { InferTypeOf } from "@medusajs/framework/types"
import { cancelPreordersWorkflow, CancelPreordersWorkflowInput } from "../workflows/cancel-preorders"
import { Preorder } from "../modules/preorder/models/preorder"

export default async function orderCanceledHandler({
  event: { data },
  container,
}: SubscriberArgs<{
  id: string
}>) {
  const query = container.resolve("query")

  const workflowInput: CancelPreordersWorkflowInput = {
    preorders: [],
    order_id: data.id,
  }
  const limit = 1000
  let offset = 0
  let count = 0

  do {
    const { 
      data: preorders,
      metadata,
    } = await query.graph({
      entity: "preorder",
      fields: ["*"],
      filters: {
        order_id: data.id,
        status: "pending",
      },
      pagination: {
        take: limit,
        skip: offset,
      },
    })
    offset += limit
    count = metadata?.count || 0

    workflowInput.preorders.push(
      ...preorders as InferTypeOf<typeof Preorder>[]
    )
  } while (count > offset + limit)
    
  await cancelPreordersWorkflow(container).run({
    input: workflowInput,
  })
}

export const config: SubscriberConfig = {
  event: "order.canceled",
}
```

The subscriber receives the ID of the canceled order in the event payload.

In the subscriber, you retrieve the preorders of the order with pagination. Then, you execute the `cancelPreordersWorkflow`, passing it the order ID and pre-orders.

### Test Order Cancelation

To test order cancelation, start the Medusa application.

Then, cancel an order that has a pre-order item. If you refresh the page, the pre-order item's status will be changed to `canceled` as well.

![Pre-order item's status is canceled in a canceled order](https://res.cloudinary.com/dza7lstvk/image/upload/v1752752393/Medusa%20Resources/CleanShot_2025-07-17_at_14.38.50_2x_cxqscy.png)

***

## Step 11: Handle Order Edits

Admin users can edit orders to add or remove items. You should handle those scenarios to:

- Cancel a pre-order if its item is removed from the order.
- Create a pre-order for a new item associated with a pre-order variant.

In this step, you'll create a workflow that cancels or creates pre-orders based on changes in an order. Then, you'll execute the workflow in a subscriber whenever an order edit is confirmed.

### a. Handle Order Edit Workflow

The workflow that will handle order edits has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the order's items with their pre-order variants.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the pre-orders of the order.
- [retrievePreorderUpdatesStep](#retrievePreorderUpdatesStep): Retrieve the pre-orders that need to be created and canceled.
- [updatePreordersStep](#updatePreordersStep): Update the status of orders to be canceled.
- [createPreordersStep](#createPreordersStep): Create pre-orders for new pre-order items.
- [emitEventStep](#emitEventStep): Emit events about canceled pre-orders.

You only need to implement the `retrievePreorderUpdatesStep`.

#### retrievePreorderUpdatesStep

The `retrievePreorderUpdatesStep` retrieves the pre-orders to be canceled and those to be created.

Create the file `src/workflows/steps/retrieve-preorder-updates.ts` with the following content:

```ts title="src/workflows/steps/retrieve-preorder-updates.ts" highlights={retrievePreorderUpdatesStepHighlights}
import { InferTypeOf, OrderDTO, OrderLineItemDTO, ProductVariantDTO } from "@medusajs/framework/types"
import { PreorderVariant } from "../../modules/preorder/models/preorder-variant"
import { Preorder, PreorderStatus } from "../../modules/preorder/models/preorder"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

export type RetrievePreorderUpdatesStep = {
  order: OrderDTO & {
    items: (OrderLineItemDTO & {
      variant?: ProductVariantDTO & {
        preorder_variant?: InferTypeOf<typeof PreorderVariant>
      }
    })[]
  }
  preorders?: InferTypeOf<typeof Preorder>[]
}

export const retrievePreorderUpdatesStep = createStep(
  "retrieve-preorder-updates",
  async ({ order, preorders }: RetrievePreorderUpdatesStep) => {
    const preordersToCancel: {
      id: string
      status: PreorderStatus.CANCELLED
    }[] = []
    const preordersToCreate: {
      preorder_variant_ids: string[]
      order_id: string
    } = {
      preorder_variant_ids: [],
      order_id: order.id,
    }

    for (const item of order.items) {
      if (
        !item.variant?.preorder_variant || 
        item.variant.preorder_variant.status === "disabled" || 
        item.variant.preorder_variant.available_date < new Date()
      ) {
        continue
      }
      const preorder = preorders?.find(
        (p) => p.item.variant_id === item.variant_id
      )
      if (!preorder) {
        preordersToCreate.preorder_variant_ids.push(
          item.variant.preorder_variant.id
        )
      }
    }

    for (const preorder of (preorders || [])) {
      const item = order.items.find(
        (i) => i.variant_id === preorder.item.variant_id
      )
      if (!item) {
        preordersToCancel.push({
          id: preorder.id,
          status: PreorderStatus.CANCELLED,
        })
      }
    }

    return new StepResponse({
      preordersToCancel,
      preordersToCreate,
    })
  }
)
```

The step receives as input the details of the order, its items, and its pre-orders.

In the step, you loop through the order's items to find pre-order variants that don't have associated pre-orders. You add those to the array of pre-orders to create.

You also loop through the pre-orders to find those that don't have associated items in the order. You add those to the array of pre-orders to cancel.

#### Create Workflow

You can now create the workflow that handles order edits.

Create the file `src/workflows/handle-order-edit.ts` with the following content:

```ts title="src/workflows/handle-order-edit.ts" highlights={handleOrderEditWorkflowHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { emitEventStep, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { RetrievePreorderUpdatesStep, retrievePreorderUpdatesStep } from "./steps/retrieve-preorder-updates"
import { updatePreordersStep } from "./steps/update-preorders"
import { createPreordersStep } from "./steps/create-preorders"

type WorkflowInput = {
  order_id: string
}

export const handleOrderEditWorkflow = createWorkflow(
  "handle-order-edit",
  (input: WorkflowInput) => {
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "*",
        "items",
        "items.variant.*",
        "items.variant.preorder_variant.*",
      ],
      filters: {
        id: input.order_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const { data: preorders } = useQueryGraphStep({
      entity: "preorder",
      fields: ["*", "item.*"],
      filters: {
        order_id: input.order_id,
        status: "pending",
      },
    }).config({ name: "retrieve-preorders" })

    const { preordersToCancel, preordersToCreate } = retrievePreorderUpdatesStep({
      order: orders[0],
      preorders,
    } as unknown as RetrievePreorderUpdatesStep)

    updatePreordersStep(preordersToCancel)
    
    createPreordersStep(preordersToCreate)

    const preordersCancelledEvent = transform({
      preordersToCancel,
      input,
    }, (data) => {
      return data.preordersToCancel.map((preorder) => ({
        id: preorder.id,
        order_id: data.input.order_id,
      }))
    })

    emitEventStep({
      eventName: "preorder.cancelled",
      data: preordersCancelledEvent,
    })

    return new WorkflowResponse({
      createdPreorders: preordersToCreate,
      cancelledPreorders: preordersToCancel,
    })
  }
)
```

The workflow receives the order ID as an input.

In the workflow, you:

1. Retrieve the order details using the [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md).
2. Retrieve the pre-orders of the order using the [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md).
3. Retrieve the pre-orders to cancel and create using the `retrievePreorderUpdatesStep`.
4. Update the pre-orders to cancel using the `updatePreordersStep`.
5. Create the pre-orders to create using the `createPreordersStep`.
6. Emit the `preorder.cancelled` event for the pre-orders that were canceled using the [emitEventStep](https://docs.medusajs.com/references/helper-steps/emitEventStep/index.html.md).
   - You can handle the event similar to the [preorder.fulfilled](#optional-send-notification-to-customer) event to notify the customer.
7. Return the created and canceled pre-orders in the response.

### b. Handle Order Edit Subscriber

Next, you'll create the subscriber that listens to the `order-edit.confirmed` event and executes the workflow you created.

Create the file `src/subscribers/order-edit-confirmed.ts` with the following content:

```ts title="src/subscribers/order-edit-confirmed.ts"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { OrderChangeActionDTO } from "@medusajs/framework/types"
import { handleOrderEditWorkflow } from "../workflows/handle-order-edit"

export default async function orderEditConfirmedHandler({
  event: { data },
  container,
}: SubscriberArgs<{
  order_id: string,
  actions: OrderChangeActionDTO[]
}>) {
  await handleOrderEditWorkflow(container).run({
    input: {
      order_id: data.order_id,
    },
  })
}

export const config: SubscriberConfig = {
  event: "order-edit.confirmed",
}
```

The subscriber receives the order ID and the actions performed in the order edit in the event payload.

You execute the `handleOrderEditWorkflow`, passing it the order ID.

### Test Order Edit Handler

To test out the order edit handler, start the Medusa application.

Then, open an order in the Medusa Admin. It can be an order you want to remove a pre-order item from, or an order you want to add a new pre-order item to.

To edit the order:

1. Click the <InlineIcon Icon={EllipsisHorizontal} alt="three-dots" /> icon at the top right of the order details section.
2. Choose Edit from the dropdown.
3. In the order edit form, you can add or remove items from the order.
   - Learn more about the order edit form in the [user guide](https://docs.medusajs.com/user-guide/orders/edit/index.html.md).
4. Click the "Confirm Edit" button at the bottom of the form, then click "Continue" in the confirmation modal.
5. The order edit request will be shown at the top of the page. Click the "Force confirm" button to confirm the order edit.

If you refresh the page, you'll see that the pre-order items were updated accordingly.

![Pre-order items updated in an order edit](https://res.cloudinary.com/dza7lstvk/image/upload/v1752754220/Medusa%20Resources/CleanShot_2025-07-17_at_15.09.55_2x_xkkfef.png)

***

## Next Steps

You've now implemented the pre-order feature in Medusa. You can expand on this feature based on your use case. You can add features like:

- Customize the storefront to show a badge for pre-order items or timers for when a pre-order item will be available.
- Automate partially capturing the payment when the order is placed.
- Add a feature to allow customers to cancel their pre-orders from the storefront.

### Learn More about Medusa

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.


# Implement Product Builder in Medusa

In this tutorial, you'll learn how to implement a product builder in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) that are available out-of-the-box.

A product builder allows customers to customize the product before adding it to the cart. This may include entering custom options like engraving text, adding complementary products to the cart, or purchasing add-ons with the product, such as insurance.

## Summary

By following this tutorial, you will learn how to:

- Install and set up Medusa with the Next.js Starter Storefront.
- Define and manage data models useful for the product builder.
- Allow admin users to manage the builder configurations of a product from Medusa Admin.
- Customize the storefront to allow customers to choose a product's builder configurations.
- Customize cart and order pages on the storefront to reflect the selected builder configurations of items.
- Customize the order details page on the Medusa Admin to reflect the selected builder configurations of items.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Screenshot of how the product builder will look like in the storefront](https://res.cloudinary.com/dza7lstvk/image/upload/v1755101580/Medusa%20Resources/CleanShot_2025-08-13_at_19.12.48_2x_tf4goa.png)

- [Full Code](https://github.com/medusajs/examples/tree/main/product-builder): Find the full code for this tutorial in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1755010179/OpenApi/Product-Builder_wvhqtq.yaml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.

Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Product Builder Module

In Medusa, you can build custom features in a [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a reusable package with the data models and functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more.

In this step, you'll build a Product Builder Module that defines the data models and logic to manage product builder configurations. The module will support three types of configurations:

1. **Custom Fields**: Allow customers to enter personalized information like engraving text or custom messages for the product.
2. **Complementary Products**: Suggest related products that enhance the main product, like keyboards with computers.
3. **Add-ons**: Optional extras like warranties, insurance, or premium features that customers can purchase alongside the main product.

### a. Create Module Directory

Create the directory `src/modules/product-builder` that will hold the Product Builder Module's code.

### b. Create Data Models

A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

Refer to the [Data Models](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md) documentation to learn more.

For the Product Builder Module, you'll define four data models to represent the different aspects of product customization.

#### ProductBuilder Data Model

The first data model will hold the main builder configurations for a product. It will have relations to the custom fields, complementary products, and add-ons.

To create the data model, create the file `src/modules/product-builder/models/product-builder.ts` with the following content:

```ts title="src/modules/product-builder/models/product-builder.ts" highlights={productBuilderHighlights}
import { model } from "@medusajs/framework/utils"
import ProductBuilderCustomField from "./product-builder-custom-field"
import ProductBuilderComplementary from "./product-builder-complementary"
import ProductBuilderAddon from "./product-builder-addon"

const ProductBuilder = model.define("product_builder", {
  id: model.id().primaryKey(),
  product_id: model.text().unique(),
  custom_fields: model.hasMany(() => ProductBuilderCustomField, {
    mappedBy: "product_builder",
  }),
  complementary_products: model.hasMany(() => ProductBuilderComplementary, {
    mappedBy: "product_builder",
  }),
  addons: model.hasMany(() => ProductBuilderAddon, {
    mappedBy: "product_builder",
  }),
})

export default ProductBuilder
```

The `ProductBuilder` data model has the following properties:

- `id`: The primary key of the table.
- `product_id`: The ID of the product that this builder configuration applies to.
  - Later, you'll learn how to link this data model to Medusa's `Product` data model.
- `custom_fields`: Fields that the customer can personalize.
- `complementary_products`: Products to suggest alongside the main product.
- `addons`: Products that the customer can buy alongside the main product.

Ignore the type errors for the related data models. You'll create them next.

Learn more about defining data model properties in the [Property Types documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md).

#### ProductBuilderCustomField Data Model

The `ProductBuilderCustomField` data model represents fields that the customer can personalize. For example, engraving text or custom messages.

To create the data model, create the file `src/modules/product-builder/models/product-builder-custom-field.ts` with the following content:

```ts title="src/modules/product-builder/models/product-builder-custom-field.ts" highlights={customFieldHighlights}
import { model } from "@medusajs/framework/utils"
import ProductBuilder from "./product-builder"

const ProductBuilderCustomField = model.define("product_builder_custom_field", {
  id: model.id().primaryKey(),
  name: model.text(),
  type: model.text(),
  description: model.text().nullable(),
  is_required: model.boolean().default(false),
  product_builder: model.belongsTo(() => ProductBuilder, {
    mappedBy: "custom_fields",
  }),
})

export default ProductBuilderCustomField
```

The `ProductBuilderCustomField` data model has the following properties:

- `id`: The primary key of the table.
- `name`: The display name shown to customers (for example, "Engraving Text" or "Custom Message").
- `type`: The input type, such as `text` or `number`.
- `description`: Optional helper text to guide customers (for example, "Enter your name to be engraved").
- `is_required`: Whether customers must fill this field before adding the product to cart.
- `product_builder`: A relation back to the parent `ProductBuilder` configuration.

#### ProductBuilderComplementary Data Model

The `ProductBuilderComplementary` data model represents products that are suggested alongside the main product. For example, if you're selling an iPad, you can suggest a keyboard to be purchased together.

To create the data model, create the file `src/modules/product-builder/models/product-builder-complementary.ts` with the following content:

```ts title="src/modules/product-builder/models/product-builder-complementary.ts" highlights={complementaryHighlights}
import { model } from "@medusajs/framework/utils"
import ProductBuilder from "./product-builder"

const ProductBuilderComplementary = model.define("product_builder_complementary", {
  id: model.id().primaryKey(),
  product_id: model.text(),
  product_builder: model.belongsTo(() => ProductBuilder, {
    mappedBy: "complementary_products",
  }),
})

export default ProductBuilderComplementary
```

The `ProductBuilderComplementary` data model has the following properties:

- `id`: The primary key of the table.
- `product_id`: The ID of the complementary product to suggest.
  - Later, you'll learn how to link this to Medusa's `Product` data model.
- `product_builder`: A relation back to the parent `ProductBuilder` configuration.

#### ProductBuilderAddon Data Model

The last data model you'll implement is the `ProductBuilderAddon` data model, which represents optional add-on products like warranties or premium features. Add-ons are typically only sold with the main product.

To create the data model, create the file `src/modules/product-builder/models/product-builder-addon.ts` with the following content:

```ts title="src/modules/product-builder/models/product-builder-addon.ts" highlights={addonHighlights}
import { model } from "@medusajs/framework/utils"
import ProductBuilder from "./product-builder"

const ProductBuilderAddon = model.define("product_builder_addon", {
  id: model.id().primaryKey(),
  product_id: model.text(),
  product_builder: model.belongsTo(() => ProductBuilder, {
    mappedBy: "addons",
  }),
})

export default ProductBuilderAddon
```

The `ProductBuilderAddon` data model has the following properties:

- `id`: The primary key of the table.
- `product_id`: The ID of the add-on product (for example, warranty or insurance product).
  - Later, you'll learn how to link this to Medusa's `Product` data model.
- `product_builder`: A relation back to the parent `ProductBuilder` configuration.

### c. Create Module's Service

You can manage your module's data models in a service.

A service is a TypeScript class that the module exports. In the service's methods, you can connect to the database, allowing you to manage your data models, or connect to a third-party service, which is useful if you're integrating with external services.

Refer to the [Module Service documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md) to learn more.

To create the Product Builder Module's service, create the file `src/modules/product-builder/service.ts` with the following content:

```ts title="src/modules/product-builder/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import ProductBuilder from "./models/product-builder"
import ProductBuilderCustomField from "./models/product-builder-custom-field"
import ProductBuilderComplementary from "./models/product-builder-complementary"
import ProductBuilderAddon from "./models/product-builder-addon"

class ProductBuilderModuleService extends MedusaService({
  ProductBuilder,
  ProductBuilderCustomField,
  ProductBuilderComplementary,
  ProductBuilderAddon,
}) {}

export default ProductBuilderModuleService
```

The `ProductBuilderModuleService` extends `MedusaService`, which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods.

So, the `ProductBuilderModuleService` class now has methods like `createProductBuilders` and `retrieveProductBuilder`.

Find all methods generated by the `MedusaService` in [the Service Factory](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md) reference.

### d. Create the Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/product-builder/index.ts` with the following content:

```ts title="src/modules/product-builder/index.ts" highlights={moduleHighlights}
import Service from "./service"
import { Module } from "@medusajs/framework/utils"

export const PRODUCT_BUILDER_MODULE = "productBuilder"

export default Module(PRODUCT_BUILDER_MODULE, {
  service: Service,
})
```

You use the `Module` function to create the module's definition. It accepts two parameters:

1. The module's name, which is `productBuilder`.
2. An object with a required property `service` indicating the module's service.

You also export the module's name as `PRODUCT_BUILDER_MODULE` so you can reference it later.

### Add Module to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/product-builder",
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

### Generate Migrations

Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript class that defines database changes made by a module.

Refer to the [Migrations documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md) to learn more.

Medusa's CLI tool can generate the migrations for you. To generate a migration for the Product Builder Module, run the following command in your Medusa application's directory:

```bash
npx medusa db:generate productBuilder
```

The `db:generate` command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a `migrations` directory under `src/modules/product-builder` that holds the generated migration.

Then, to reflect these migrations on the database, run the following command:

```bash
npx medusa db:migrate
```

The tables for the data models are now created in the database.

***

## Step 3: Define Links between Data Models

Since Medusa isolates modules to integrate them into your application without side effects, you can't directly create relationships between data models of different modules.

Instead, Medusa provides a mechanism to define links between data models, and retrieve and manage linked records while maintaining module isolation.

Refer to the [Module Links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md) documentation to learn more about defining links.

In this step, you'll define links between the data models in the Product Builder Module and the data models in Medusa's Product Module:

- `ProductBuilder` ↔ `Product`: A product builder record represents the builder configurations of a product.
- `ProductBuilderComplementary` ↔ `Product`: A complementary product record suggests a Medusa product related to the main product.
- `ProductBuilderAddon` ↔ `Product`: An add-on product record suggests a Medusa product that can be added to the main product in the cart.

### a. ProductBuilder ↔ Product

To define a link between the `ProductBuilder` and `Product` data models, create the file `src/links/product-builder-product.ts` with the following content:

```ts title="src/links/product-builder-product.ts" highlights={productBuilderLinkHighlights}
import ProductBuilderModule from "../modules/product-builder"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: ProductBuilderModule.linkable.productBuilder,
    deleteCascade: true,
  },
  ProductModule.linkable.product
)
```

You define a link using the `defineLink` function. It accepts two parameters:

1. An object indicating the first data model part of the link. A module has a special `linkable` property that contains link configurations for its data models. You pass the linkable configurations of the Product Builder Module's `ProductBuilder` data model, and you enable the `deleteCascade` option to automatically delete the builder configuration when the product is deleted.
2. An object indicating the second data model part of the link. You pass the linkable configurations of the Product Module's `Product` data model.

### b. ProductBuilderComplementary ↔ Product

Next, to define a link between the `ProductBuilderComplementary` and `Product` data models, create the file `src/links/product-builder-complementary-product.ts` with the following content:

```ts title="src/links/product-builder-complementary-product.ts" highlights={complementaryLinkHighlights}
import ProductBuilderModule from "../modules/product-builder"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: ProductBuilderModule.linkable.productBuilderComplementary,
    deleteCascade: true,
  },
  ProductModule.linkable.product
)
```

You define a link similarly to the previous one. You also enable the `deleteCascade` option to automatically delete the complementary product record when the main product is deleted.

### c. ProductBuilderAddon ↔ Product

Finally, to define a link between the `ProductBuilderAddon` and `Product` data models, create the file `src/links/product-builder-addon-product.ts` with the following content:

```ts title="src/links/product-builder-addon-product.ts" highlights={addonLinkHighlights}
import ProductBuilderModule from "../modules/product-builder"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: ProductBuilderModule.linkable.productBuilderAddon,
    deleteCascade: true,
  },
  ProductModule.linkable.product
)
```

Similarly to the previous links, you define a link between the `ProductBuilderAddon` and `Product` data models. You also enable the `deleteCascade` option to automatically delete the add-on product record when the main product is deleted.

### d. Sync Links to Database

After defining links, you need to sync them to the database. This creates the necessary tables to store the links.

To sync the links to the database, run the migrations command again in the Medusa application's directory:

```bash
npx medusa db:migrate
```

This command will create the necessary tables to store the links between your Product Builder Module and Medusa's Product Module.

***

## Step 4: Manage Product Builder Configurations

In this step, you'll implement the logic to manage product builder configurations. You'll also expose this functionality to clients, allowing you later to use it in the admin dashboard customizations.

To implement the product builder management functionality, you'll create:

- A [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) to create or update (upsert) product builder configurations.
- An [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) to expose the workflow's functionality to client applications.

### a. Upsert Product Builder Workflow

The first workflow you'll implement creates or updates builder configurations for a product.

A workflow is a series of queries and actions, called steps, that complete a task. A workflow is similar to a function, but it allows you to track its executions' progress, define roll-back logic, and configure other advanced features.

Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) documentation to learn more.

The workflow you'll build will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the product builder configuration, if it exists.
- [prepareProductBuilderCustomFieldsStep](#prepareProductBuilderCustomFieldsStep): Prepare custom fields to create, update, or delete.
- [createProductBuilderCustomFieldsStep](#createProductBuilderCustomFieldsStep): Create custom fields for the product builder.
- [updateProductBuilderCustomFieldsStep](#updateProductBuilderCustomFieldsStep): Update custom fields for the product builder.
- [deleteProductBuilderCustomFieldsStep](#deleteProductBuilderCustomFieldsStep): Delete custom fields for the product builder.
- [prepareProductBuilderComplementaryProductsStep](#prepareProductBuilderComplementaryProductsStep): Prepare complementary products to create, or delete.
- [createProductBuilderComplementaryProductsStep](#createProductBuilderComplementaryProductsStep): Create complementary products for the product builder.
- [deleteProductBuilderComplementaryProductsStep](#deleteProductBuilderComplementaryProductsStep): Delete complementary products for the product builder.
- [prepareProductBuilderAddonsStep](#prepareProductBuilderAddonsStep): Prepare addon products to create, or delete.
- [createProductBuilderAddonsStep](#createProductBuilderAddonsStep): Create addon products for the product builder.
- [deleteProductBuilderAddonsStep](#deleteProductBuilderAddonsStep): Delete addon products for the product builder.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the updated product builder configuration.

The `useQueryGraphStep`, `createRemoteLinkStep`, and `dismissRemoteLinkStep` are available through Medusa's `@medusajs/medusa/core-flows` package. You'll implement other steps in the workflow.

#### createProductBuilderStep

The `createProductBuilderStep` creates a new product builder configuration.

To create the step, create the file `src/workflows/steps/create-product-builder.ts` with the following content:

If you get a type error on resolving the Product Builder Module, run the Medusa application once with the `npm run dev` or `yarn dev` command to generate the necessary type definitions, as explained in the [Automatically Generated Types guide](https://docs.medusajs.com/docs/learn/fundamentals/generated-types/index.html.md).

```ts title="src/workflows/steps/create-product-builder.ts" highlights={createProductBuilderStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PRODUCT_BUILDER_MODULE } from "../../modules/product-builder"

export type CreateProductBuilderStepInput = {
  product_id: string
}

export const createProductBuilderStep = createStep(
  "create-product-builder",
  async (input: CreateProductBuilderStepInput, { container }) => {
    const productBuilderModuleService = container.resolve(PRODUCT_BUILDER_MODULE)

    const productBuilder = await productBuilderModuleService.createProductBuilders({
      product_id: input.product_id,
    })

    return new StepResponse(productBuilder, productBuilder)
  },
  async (productBuilder, { container }) => {
    if (!productBuilder) {return}

    const productBuilderModuleService = container.resolve(PRODUCT_BUILDER_MODULE)
    
    await productBuilderModuleService.deleteProductBuilders(productBuilder.id)
  }
)
```

You create a step with the `createStep` function. It accepts three parameters:

1. The step's unique name.
2. An async function that receives two parameters:
   - The step's input, which is an object with the product builder's properties.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.
3. An async compensation function that undoes the actions performed by the step function. This function is only executed if an error occurs during the workflow's execution.

In the step function, you resolve the Product Builder Module's service from the Medusa container and create a product builder record.

A step function must return a `StepResponse` instance with the step's output, which is the created product builder record in this case.

You also pass the product builder record to the compensation function, which deletes the product builder record if an error occurs during the workflow's execution.

#### prepareProductBuilderCustomFieldsStep

The `prepareProductBuilderCustomFieldsStep` receives the custom fields from the workflow's input and returns which custom fields should be created, updated, or deleted.

To create the step, create the file `src/workflows/steps/prepare-product-builder-custom-fields.ts` with the following content:

```ts title="src/workflows/steps/prepare-product-builder-custom-fields.ts" highlights={prepareCustomFieldsStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PRODUCT_BUILDER_MODULE } from "../../modules/product-builder"

export type PrepareProductBuilderCustomFieldsStepInput = {
  product_builder_id: string
  custom_fields?: Array<{
    id?: string
    name: string
    type: string
    is_required?: boolean
    description?: string | null
  }>
}

export const prepareProductBuilderCustomFieldsStep = createStep(
  "prepare-product-builder-custom-fields",
  async (input: PrepareProductBuilderCustomFieldsStepInput, { container }) => {
    const productBuilderModuleService = container.resolve(PRODUCT_BUILDER_MODULE)

    // Get existing custom fields for this product builder
    const existingCustomFields = await productBuilderModuleService.listProductBuilderCustomFields({
      product_builder_id: input.product_builder_id,
    })

    // Separate operations: create, update, and delete
    const toCreate: any[] = []
    const toUpdate: any[] = []

    // TODO determine the fields to create, update, or delete
  }
)
```

The step function receives as an input the product builder ID and the custom fields to manage.

In the step, you resolve the Product Builder Module's service and retrieve the existing custom fields associated with the product builder.

Then, you prepare arrays to hold the fields to create and update.

Next, you need to check which custom fields should be created, updated, or deleted based on the input and existing custom fields.

Replace the `TODO` with the following:

```ts title="src/workflows/steps/prepare-product-builder-custom-fields.ts" highlights={prepareCustomFieldsStepHighlights2}
// Process input fields to determine creates vs updates
input.custom_fields?.forEach((fieldData) => {
  const existingField = existingCustomFields.find((f) => f.id === fieldData.id)
  if (fieldData.id && existingField) {
    // Update existing field
    toUpdate.push({
      id: fieldData.id,
      name: fieldData.name,
      type: fieldData.type,
      is_required: fieldData.is_required ?? false,
      description: fieldData.description ?? "",
    })
  } else {
    // Create new field
    toCreate.push({
      product_builder_id: input.product_builder_id,
      name: fieldData.name,
      type: fieldData.type,
      is_required: fieldData.is_required ?? false,
      description: fieldData.description ?? "",
    })
  }
})

// Find fields to delete (existing but not in input)
const toDelete = existingCustomFields.filter(
  (field) => !input.custom_fields?.some((f) => f.id === field.id)
)

return new StepResponse({
  toCreate,
  toUpdate,
  toDelete,
})
```

You loop over the `custom_fields` array in the input to determine which fields need to be created or updated, then you add them to the appropriate arrays.

Afterwards, you find the fields that need to be deleted by checking which existing fields are not present in the input.

Finally, you return an object that has the custom fields to create, update, and delete.

#### createProductBuilderCustomFieldsStep

The `createProductBuilderCustomFieldsStep` creates custom fields.

To create the step, create the file `src/workflows/steps/create-product-builder-custom-fields.ts` with the following content:

```ts title="src/workflows/steps/create-product-builder-custom-fields.ts" highlights={createCustomFieldsStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PRODUCT_BUILDER_MODULE } from "../../modules/product-builder"

export type CreateProductBuilderCustomFieldsStepInput = {
  custom_fields: Array<{
    product_builder_id: string
    name: string
    type: string
    is_required: boolean
    description?: string
  }>
}

export const createProductBuilderCustomFieldsStep = createStep(
  "create-product-builder-custom-fields",
  async (input: CreateProductBuilderCustomFieldsStepInput, { container }) => {
    const productBuilderModuleService = container.resolve(PRODUCT_BUILDER_MODULE)
    
    const createdFields = await productBuilderModuleService.createProductBuilderCustomFields(
      input.custom_fields
    )
    
    return new StepResponse(createdFields, {
      createdItems: createdFields,
    })
  },
  async (compensationData, { container }) => {
    if (!compensationData?.createdItems?.length) {
      return
    }

    const productBuilderModuleService = container.resolve(PRODUCT_BUILDER_MODULE)
    await productBuilderModuleService.deleteProductBuilderCustomFields(
      compensationData.createdItems.map((f: any) => f.id)
    )
  }
)
```

This step receives the custom fields to create as input.

In the step function, you create the custom fields and return them.

In the compensation function, you delete the created custom fields if an error occurs during the workflow's execution.

#### updateProductBuilderCustomFieldsStep

The `updateProductBuilderCustomFieldsStep` updates existing custom fields.

To create the step, create the file `src/workflows/steps/update-product-builder-custom-fields.ts` with the following content:

```ts title="src/workflows/steps/update-product-builder-custom-fields.ts" highlights={updateProductBuilderCustomFieldsStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PRODUCT_BUILDER_MODULE } from "../../modules/product-builder"

export type UpdateProductBuilderCustomFieldsStepInput = {
  custom_fields: Array<{
    id: string
    name: string
    type: string
    is_required: boolean
    description?: string
  }>
}

export const updateProductBuilderCustomFieldsStep = createStep(
  "update-product-builder-custom-fields",
  async (input: UpdateProductBuilderCustomFieldsStepInput, { container }) => {
    const productBuilderModuleService = container.resolve(PRODUCT_BUILDER_MODULE)
    
    // Store original state for compensation
    const originalFields = await productBuilderModuleService.listProductBuilderCustomFields({
      id: input.custom_fields.map((f) => f.id),
    })
    
    const updatedFields = await productBuilderModuleService.updateProductBuilderCustomFields(
      input.custom_fields
    )
    
    return new StepResponse(updatedFields, {
      originalItems: originalFields,
    })
  },
  async (compensationData, { container }) => {
    if (!compensationData?.originalItems?.length) {
      return
    }

    const productBuilderModuleService = container.resolve(PRODUCT_BUILDER_MODULE)
    await productBuilderModuleService.updateProductBuilderCustomFields(
      compensationData.originalItems.map((f: any) => ({
        id: f.id,
        name: f.name,
        type: f.type,
        is_required: f.is_required,
        description: f.description,
      }))
    )
  }
)
```

The step receives the custom fields to update as input.

In the step function, you update the custom fields and return them.

In the compensation function, you restore the custom fields to their original values if an error occurs during the workflow's execution.

#### deleteProductBuilderCustomFieldsStep

The `deleteProductBuilderCustomFieldsStep` deletes custom fields.

To create the step, create the file `src/workflows/steps/delete-product-builder-custom-fields.ts` with the following content:

```ts title="src/workflows/steps/delete-product-builder-custom-fields.ts" highlights={deleteProductBuilderCustomFieldsStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PRODUCT_BUILDER_MODULE } from "../../modules/product-builder"

export type DeleteProductBuilderCustomFieldsStepInput = {
  custom_fields: Array<{
    id: string
    product_builder_id: string
    name: string
    type: string
    is_required: boolean
    description?: string | null
  }>
}

export const deleteProductBuilderCustomFieldsStep = createStep(
  "delete-product-builder-custom-fields",
  async (input: DeleteProductBuilderCustomFieldsStepInput, { container }) => {
    const productBuilderModuleService = container.resolve(PRODUCT_BUILDER_MODULE)
    
    await productBuilderModuleService.deleteProductBuilderCustomFields(
      input.custom_fields.map((f) => f.id)
    )
    
    return new StepResponse(input.custom_fields, {
      deletedItems: input.custom_fields,
    })
  },
  async (compensationData, { container }) => {
    if (!compensationData?.deletedItems?.length) {
      return
    }

    const productBuilderModuleService = container.resolve(PRODUCT_BUILDER_MODULE)
    await productBuilderModuleService.createProductBuilderCustomFields(
      compensationData.deletedItems.map((f: any) => ({
        id: f.id,
        product_builder_id: f.product_builder_id,
        name: f.name,
        type: f.type,
        is_required: f.is_required,
        description: f.description,
      }))
    )
  }
)
```

The step receives the custom fields to delete as input.

In the step function, you delete the custom fields and return them.

In the compensation function, you restore the custom fields if an error occurs during the workflow's execution.

#### prepareProductBuilderComplementaryProductsStep

The `prepareProductBuilderComplementaryProductsStep` receives the complementary products from the workflow's input and returns which complementary products should be created or deleted.

To create the step, create the file `src/workflows/steps/prepare-product-builder-complementary-products.ts` with the following content:

```ts title="src/workflows/steps/prepare-product-builder-complementary-products.ts" highlights={prepareProductBuilderComplementaryProductsStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PRODUCT_BUILDER_MODULE } from "../../modules/product-builder"

export type PrepareProductBuilderComplementaryProductsStepInput = {
  product_builder_id: string
  complementary_products?: Array<{
    id?: string
    product_id: string
  }>
}

export const prepareProductBuilderComplementaryProductsStep = createStep(
  "prepare-product-builder-complementary-products",
  async (input: PrepareProductBuilderComplementaryProductsStepInput, { container }) => {
    const productBuilderModuleService = container.resolve(PRODUCT_BUILDER_MODULE)

    // Get existing complementary products for this product builder
    const existingComplementaryProducts = await productBuilderModuleService
      .listProductBuilderComplementaries({
        product_builder_id: input.product_builder_id,
      })

    // Separate operations: create and delete
    const toCreate: any[] = []

    // Process input products to determine creates
    input.complementary_products?.forEach((productData) => {
      const existingProduct = existingComplementaryProducts.find(
        (p) => p.product_id === productData.product_id
      )
      if (!existingProduct) {
        // Create new complementary product
        toCreate.push({
          product_builder_id: input.product_builder_id,
          product_id: productData.product_id,
        })
      }
    })

    // Find products to delete (existing but not in input)
    const toDelete = existingComplementaryProducts.filter(
      (product) => !input.complementary_products?.some(
        (p) => p.product_id === product.product_id
      )
    )

    return new StepResponse({
      toCreate,
      toDelete,
    })
  }
)
```

The step receives the ID of the product builder and the complementary products to manage as input.

In the step, you retrieve the existing complementary products for the specified product builder and determine which products need to be created or deleted based on whether it exists in the input.

You return an object that has the complementary products to create and delete.

#### createProductBuilderComplementaryProductsStep

The `createProductBuilderComplementaryProductsStep` creates complementary products.

To create the step, create the file `src/workflows/steps/create-product-builder-complementary-products.ts` with the following content:

```ts title="src/workflows/steps/create-product-builder-complementary-products.ts" highlights={createProductBuilderComplementaryProductsStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PRODUCT_BUILDER_MODULE } from "../../modules/product-builder"

export type CreateProductBuilderComplementaryProductsStepInput = {
  complementary_products: Array<{
    product_builder_id: string
    product_id: string
  }>
}

export const createProductBuilderComplementaryProductsStep = createStep(
  "create-product-builder-complementary-products",
  async (input: CreateProductBuilderComplementaryProductsStepInput, { container }) => {
    const productBuilderModuleService = container.resolve(PRODUCT_BUILDER_MODULE)
    
    const created = await productBuilderModuleService.createProductBuilderComplementaries(
      input.complementary_products
    )
    const createdArray = Array.isArray(created) ? created : [created]
    
    return new StepResponse(createdArray, {
      createdIds: createdArray.map((p: any) => p.id),
    })
  },
  async (compensationData, { container }) => {
    if (!compensationData?.createdIds?.length) {
      return
    }

    const productBuilderModuleService = container.resolve(PRODUCT_BUILDER_MODULE)
    await productBuilderModuleService.deleteProductBuilderComplementaries(
      compensationData.createdIds
    )
  }
)
```

The step receives the complementary products to create as input.

In the step, you create the complementary products and return them.

In the compensation function, you delete the created complementary products if an error occurs during the workflow's execution.

#### deleteProductBuilderComplementaryProductsStep

The `deleteProductBuilderComplementaryProductsStep` deletes complementary products.

To create the step, create the file `src/workflows/steps/delete-product-builder-complementary-products.ts` with the following content:

```ts title="src/workflows/steps/delete-product-builder-complementary-products.ts" highlights={deleteProductBuilderComplementaryProductsStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PRODUCT_BUILDER_MODULE } from "../../modules/product-builder"

export type DeleteProductBuilderComplementaryProductsStepInput = {
  complementary_products: Array<{
    id: string
    product_id: string
    product_builder_id: string
  }>
}

export const deleteProductBuilderComplementaryProductsStep = createStep(
  "delete-product-builder-complementary-products",
  async (input: DeleteProductBuilderComplementaryProductsStepInput, { container }) => {
    const productBuilderModuleService = container.resolve(PRODUCT_BUILDER_MODULE)
    
    await productBuilderModuleService.deleteProductBuilderComplementaries(
      input.complementary_products.map((p) => p.id)
    )
    
    return new StepResponse(input.complementary_products, {
      deletedItems: input.complementary_products,
    })
  },
  async (compensationData, { container }) => {
    if (!compensationData?.deletedItems?.length) {
      return
    }

    const productBuilderModuleService = container.resolve(PRODUCT_BUILDER_MODULE)
    await productBuilderModuleService.createProductBuilderComplementaries(
      compensationData.deletedItems.map((p: any) => ({
        id: p.id,
        product_builder_id: p.product_builder_id,
        product_id: p.product_id,
      }))
    )
  }
)
```

This step receives complementary products to delete as input.

In the step, you delete the complementary products.

In the compensation function, you recreate the deleted complementary products if an error occurs during the workflow's execution.

#### prepareProductBuilderAddonsStep

The `prepareProductBuilderAddonsStep` receives the addon products from the workflow's input and returns which addon products should be created or deleted.

To create the step, create the file `src/workflows/steps/prepare-product-builder-addons.ts` with the following content:

```ts title="src/workflows/steps/prepare-product-builder-addons.ts" highlights={prepareProductBuilderAddonsStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PRODUCT_BUILDER_MODULE } from "../../modules/product-builder"

export type PrepareProductBuilderAddonsStepInput = {
  product_builder_id: string
  addon_products?: Array<{
    id?: string
    product_id: string
  }>
}

export const prepareProductBuilderAddonsStep = createStep(
  "prepare-product-builder-addons",
  async (input: PrepareProductBuilderAddonsStepInput, { container }) => {
    const productBuilderModuleService = container.resolve(PRODUCT_BUILDER_MODULE)

    // Get existing addon associations for this product builder
    const existingAddons = await productBuilderModuleService.listProductBuilderAddons({
      product_builder_id: input.product_builder_id,
    })

    // Separate operations: create, update, and delete
    const toCreate: any[] = []

    // Process input products to determine creates
    input.addon_products?.forEach((productData) => {
      const existingAddon = existingAddons.find(
        (a) => a.product_id === productData.product_id
      )
      if (!existingAddon) {
        // Create new addon product
        toCreate.push({
          product_builder_id: input.product_builder_id,
          product_id: productData.product_id,
        })
      }
    })

    // Find products to delete (existing but not in input)
    const toDelete = existingAddons.filter(
      (product) => !input.addon_products?.some(
        (p) => p.product_id === product.product_id
      )
    )

    return new StepResponse({
      toCreate,
      toDelete,
    })
  }
)
```

The step receives the ID of the product builder and the addon products to manage as input.

In the step, you retrieve the existing addon products for the specified product builder and determine which products need to be created or deleted based on whether it exists in the input.

You return an object that has the addon products to create and delete.

#### createProductBuilderAddonsStep

The `createProductBuilderAddonsStep` creates addon products.

To create the step, create the file `src/workflows/steps/create-product-builder-addons.ts` with the following content:

```ts title="src/workflows/steps/create-product-builder-addons.ts" highlights={createProductBuilderAddonsStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PRODUCT_BUILDER_MODULE } from "../../modules/product-builder"

export type CreateProductBuilderAddonsStepInput = {
  addon_products: Array<{
    product_builder_id: string
    product_id: string
  }>
}

export const createProductBuilderAddonsStep = createStep(
  "create-product-builder-addons",
  async (input: CreateProductBuilderAddonsStepInput, { container }) => {
    const productBuilderModuleService = container.resolve(PRODUCT_BUILDER_MODULE)
    
    const createdAddons = await productBuilderModuleService.createProductBuilderAddons(
      input.addon_products
    )
    
    return new StepResponse(createdAddons, {
      createdItems: createdAddons,
    })
  },
  async (compensationData, { container }) => {
    if (!compensationData?.createdItems?.length) {
      return
    }

    const productBuilderModuleService = container.resolve(PRODUCT_BUILDER_MODULE)
    await productBuilderModuleService.deleteProductBuilderAddons(
      compensationData.createdItems.map((a: any) => a.id)
    )
  }
)
```

The step receives the addon products to create as input.

In the step, you create the addon products and return them.

In the compensation function, you delete the created addon products if an error occurs during the workflow's execution.

#### deleteProductBuilderAddonsStep

The `deleteProductBuilderAddonsStep` deletes addon products.

To create the step, create the file `src/workflows/steps/delete-product-builder-addons.ts` with the following content:

```ts title="src/workflows/steps/delete-product-builder-addons.ts" highlights={deleteProductBuilderAddonsStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PRODUCT_BUILDER_MODULE } from "../../modules/product-builder"

export type DeleteProductBuilderAddonsStepInput = {
  addon_products: Array<{
    id: string
    product_builder_id: string
    product_id: string
  }>
}

export const deleteProductBuilderAddonsStep = createStep(
  "delete-product-builder-addons",
  async (input: DeleteProductBuilderAddonsStepInput, { container }) => {
    const productBuilderModuleService = container.resolve(PRODUCT_BUILDER_MODULE)
    
    await productBuilderModuleService.deleteProductBuilderAddons(
      input.addon_products.map((a) => a.id)
    )
    
    return new StepResponse(input.addon_products, {
      deletedItems: input.addon_products,
    })
  },
  async (compensationData, { container }) => {
    if (!compensationData?.deletedItems?.length) {
      return
    }

    const productBuilderModuleService = container.resolve(PRODUCT_BUILDER_MODULE)
    await productBuilderModuleService.createProductBuilderAddons(
      compensationData.deletedItems.map((a: any) => ({
        id: a.id,
        product_builder_id: a.product_builder_id,
        product_id: a.product_id,
      }))
    )
  }
)
```

This step receives addon products to delete as input.

In the step, you delete the addon products.

In the compensation function, you recreate the deleted addon products if an error occurs during the workflow's execution.

#### Create Workflow

You now have the necessary steps to build the workflow that upserts a product builder configuration. Since the workflow is long, you'll create it in chunks.

Start by creating the file `src/workflows/upsert-product-builder.ts` with the following content:

```ts title="src/workflows/upsert-product-builder.ts" collapsibleLines="1-17" expandButtonLabel="Show Imports"
import { createWorkflow, parallelize, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { createRemoteLinkStep, dismissRemoteLinkStep } from "@medusajs/medusa/core-flows"
import { Modules } from "@medusajs/framework/utils"
import { createProductBuilderStep } from "./steps/create-product-builder"
import { prepareProductBuilderCustomFieldsStep } from "./steps/prepare-product-builder-custom-fields"
import { createProductBuilderCustomFieldsStep } from "./steps/create-product-builder-custom-fields"
import { updateProductBuilderCustomFieldsStep } from "./steps/update-product-builder-custom-fields"
import { deleteProductBuilderCustomFieldsStep } from "./steps/delete-product-builder-custom-fields"
import { prepareProductBuilderComplementaryProductsStep } from "./steps/prepare-product-builder-complementary-products"
import { createProductBuilderComplementaryProductsStep } from "./steps/create-product-builder-complementary-products"
import { deleteProductBuilderComplementaryProductsStep } from "./steps/delete-product-builder-complementary-products"
import { prepareProductBuilderAddonsStep } from "./steps/prepare-product-builder-addons"
import { createProductBuilderAddonsStep } from "./steps/create-product-builder-addons"
import { deleteProductBuilderAddonsStep } from "./steps/delete-product-builder-addons"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { PRODUCT_BUILDER_MODULE } from "../modules/product-builder"

export type UpsertProductBuilderWorkflowInput = {
  product_id: string
  custom_fields?: Array<{
    id?: string
    name: string
    type: string
    is_required?: boolean
    description?: string | null
  }>
  complementary_products?: Array<{
    id?: string
    product_id: string
  }>
  addon_products?: Array<{
    id?: string
    product_id: string
  }>
}

export const upsertProductBuilderWorkflow = createWorkflow(
  "upsert-product-builder",
  (input: UpsertProductBuilderWorkflowInput) => {
    // TODO retrieve or create product builder
  }
)
```

You create a workflow using the `createWorkflow` function. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function that holds the workflow's implementation.

The function accepts an input object holding the details of the product builder to create or update, including its associated product ID, custom fields, complementary products, and addon products.

The first part of the workflow is to retrieve or create the product builder configuration. So, replace the `TODO` with the following:

```ts title="src/workflows/upsert-product-builder.ts" highlights={upsertProductBuilderWorkflowHighlights1}
const { data: existingProductBuilder } = useQueryGraphStep({
  entity: "product_builder",
  fields: [
    "id",
  ],
  filters: {
    product_id: input.product_id,
  },
})

const productBuilder = when({
  existingProductBuilder,
  // @ts-ignore
}, ({ existingProductBuilder }) => existingProductBuilder.length === 0)
  .then(() => {
    const productBuilder = createProductBuilderStep({
      product_id: input.product_id,
    })

    const productBuilderLink = transform({
      productBuilder,
    }, (data) => [{
      [PRODUCT_BUILDER_MODULE]: {
        product_builder_id: data.productBuilder!.id,
      },
      [Modules.PRODUCT]: {
        product_id: data.productBuilder!.product_id,
      },
    }])

    const link = createRemoteLinkStep(productBuilderLink)

    return productBuilder
  })

const productBuilderId = transform({
  existingProductBuilder, productBuilder,
}, (data) => data.productBuilder?.id || data.existingProductBuilder[0]!.id)

// TODO manage custom fields
```

In this snippet, you:

1. Try to retrieve the existing product builder using the `useQueryGraphStep`.
   - This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve data across modules.
2. Use [when-then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) to check whether the existing product builder was found.
   - If there's no existing product builder, you create a new one using the `createProductBuilderStep`, then link it to the product using the `createRemoteLinkStep`.
3. Use [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) to extract the product builder ID from either the existing or newly created product builder.

In a workflow, you can't manipulate data or check conditions because Medusa stores an internal representation of the workflow on application startup. Learn more in the [Data Manipulation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) and [Conditions](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) documentation.

Next, you need to manage the custom fields passed in the input. Replace the new `TODO` in the workflow with the following:

```ts title="src/workflows/upsert-product-builder.ts" highlights={upsertProductBuilderWorkflowHighlights2}
// Prepare custom fields operations
const {
  toCreate: customFieldsToCreate,
  toUpdate: customFieldsToUpdate,
  toDelete: customFieldsToDelete,
} = prepareProductBuilderCustomFieldsStep({
  product_builder_id: productBuilderId,
  custom_fields: input.custom_fields,
})

parallelize(
  createProductBuilderCustomFieldsStep({
    custom_fields: customFieldsToCreate,
  }),
  updateProductBuilderCustomFieldsStep({
    custom_fields: customFieldsToUpdate,
  }),
  deleteProductBuilderCustomFieldsStep({
    custom_fields: customFieldsToDelete,
  })
)

// TODO manage complementary products and addons
```

In this portion, you use the `prepareProductBuilderCustomFieldsStep` to determine which custom fields need to be created, updated, or deleted.

Then, you run the `createProductBuilderCustomFieldsStep`, `updateProductBuilderCustomFieldsStep`, and `deleteProductBuilderCustomFieldsStep` in parallel to manage the custom fields.

Next, you need to manage the complementary products passed in the input. Replace the new `TODO` in the workflow with the following:

```ts title="src/workflows/upsert-product-builder.ts" highlights={upsertProductBuilderWorkflowHighlights3}
// Prepare complementary products operations
const {
  toCreate: complementaryProductsToCreate,
  toDelete: complementaryProductsToDelete,
} = prepareProductBuilderComplementaryProductsStep({
  product_builder_id: productBuilderId,
  complementary_products: input.complementary_products,
})

const [
  createdComplementaryProducts,
  deletedComplementaryProducts,
] = parallelize(
  createProductBuilderComplementaryProductsStep({
    complementary_products: complementaryProductsToCreate,
  }),
  deleteProductBuilderComplementaryProductsStep({
    complementary_products: complementaryProductsToDelete,
  })
)

// Create remote links for complementary products
const {
  complementaryProductLinks,
  deletedComplementaryProductLinks,
} = transform({
  createdComplementaryProducts,
  deletedComplementaryProducts,
}, (data) => {
  return {
    complementaryProductLinks: data.createdComplementaryProducts.map((item) => ({
      [PRODUCT_BUILDER_MODULE]: {
        product_builder_complementary_id: item.id,
      },
      [Modules.PRODUCT]: {
        product_id: item.product_id,
      },
    })),
    deletedComplementaryProductLinks: data.deletedComplementaryProducts.map((item) => ({
      [PRODUCT_BUILDER_MODULE]: {
        product_builder_complementary_id: item.id,
      },
      [Modules.PRODUCT]: {
        product_id: item.product_id,
      },
    })),
  }
})

when({
  complementaryProductLinks,
}, ({ complementaryProductLinks }) => complementaryProductLinks.length > 0)
  .then(() => {
    createRemoteLinkStep(complementaryProductLinks).config({
      name: "create-complementary-product-links",
    })
  })

when({
  deletedComplementaryProductLinks,
}, ({ deletedComplementaryProductLinks }) => deletedComplementaryProductLinks.length > 0)
  .then(() => {
    dismissRemoteLinkStep(deletedComplementaryProductLinks)
  })

// TODO manage addon products
```

In this portion of the workflow, you:

- Prepare which complementary products need to be created or deleted using the `prepareProductBuilderComplementaryProductsStep`.
- Run the `createProductBuilderComplementaryProductsStep` and `deleteProductBuilderComplementaryProductsStep` in parallel to manage the complementary products.
- Prepare the links to be created or deleted between the complementary products and the Medusa products.
- Create the links for the new complementary products.
- Dismiss the links for the deleted complementary products.

Next, you need to manage the addon products passed in the input. Replace the new `TODO` in the workflow with the following:

```ts title="src/workflows/upsert-product-builder.ts" highlights={upsertProductBuilderWorkflowHighlights4}
// Prepare addons operations
const {
  toCreate: addonsToCreate,
  toDelete: addonsToDelete,
} = prepareProductBuilderAddonsStep({
  product_builder_id: productBuilderId,
  addon_products: input.addon_products,
})

const [createdAddons, deletedAddons] = parallelize(
  createProductBuilderAddonsStep({
    addon_products: addonsToCreate,
  }),
  deleteProductBuilderAddonsStep({
    addon_products: addonsToDelete,
  })
)

// Create remote links for addon products
const {
  addonProductLinks,
  deletedAddonProductLinks,
} = transform({
  createdAddons,
  deletedAddons,
}, (data) => {
  return {
    addonProductLinks: data.createdAddons.map((item) => ({
      [PRODUCT_BUILDER_MODULE]: {
        product_builder_addon_id: item.id,
      },
      [Modules.PRODUCT]: {
        product_id: item.product_id,
      },
    })),
    deletedAddonProductLinks: data.deletedAddons.map((item) => ({
      [PRODUCT_BUILDER_MODULE]: {
        product_builder_addon_id: item.id,
      },
      [Modules.PRODUCT]: {
        product_id: item.product_id,
      },
    })),
  }
})

when({
  addonProductLinks,
}, ({ addonProductLinks }) => addonProductLinks.length > 0)
  .then(() => {
    createRemoteLinkStep(addonProductLinks).config({
      name: "create-addon-product-links",
    })
  })

when({
  deletedAddonProductLinks,
}, ({ deletedAddonProductLinks }) => deletedAddonProductLinks.length > 0)
  .then(() => {
    dismissRemoteLinkStep(deletedAddonProductLinks).config({
      name: "dismiss-addon-product-links",
    })
  })
// TODO retrieve and return the product builder configuration
```

This part of the workflow is similar to the complementary products management, but it handles addon products instead. You create and delete addon products, then create and dismiss links between them and Medusa products.

Finally, you need to retrieve and return the product builder configuration. Replace the last `TODO` in the workflow with the following:

```ts title="src/workflows/upsert-product-builder.ts" highlights={upsertProductBuilderWorkflowHighlights5}
const { data: productBuilders } = useQueryGraphStep({
  entity: "product_builder",
  fields: [
    "id",
    "product_id", 
    "custom_fields.*",
    "complementary_products.*",
    "complementary_products.product.*",
    "addons.*",
    "addons.product.*",
    "created_at",
    "updated_at",
  ],
  filters: {
    product_id: input.product_id,
  },
}).config({ name: "get-product-builder" })

// @ts-ignore
return new WorkflowResponse({
  product_builder: productBuilders[0],
})
```

You retrieve the product builder configuration again using `useQueryGraphStep`.

A workflow must return an instance of `WorkflowResponse`. It receives as a parameter the data returned by the workflow, which is the product builder configuration.

### b. Upsert Product Builder API Route

Next, you'll create the API route that exposes the workflow's functionality to clients.

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

Refer to the [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) documentation to learn more about them.

Create the file `src/api/admin/products/[id]/builder/route.ts` with the following content:

```ts title="src/api/admin/products/[id]/builder/route.ts" highlights={builderRouteHighlights}
import { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework"
import { z } from "zod"
import { upsertProductBuilderWorkflow } from "../../../../../workflows/upsert-product-builder"

export const UpsertProductBuilderSchema = z.object({
  custom_fields: z.array(z.object({
    id: z.string().optional(),
    name: z.string(),
    type: z.string(),
    is_required: z.boolean().optional().default(false),
    description: z.string().nullable().optional(),
  })).optional(),
  complementary_products: z.array(z.object({
    id: z.string().optional(),
    product_id: z.string(),
  })).optional(),
  addon_products: z.array(z.object({
    id: z.string().optional(),
    product_id: z.string(),
  })).optional(),
})

export const POST = async (
  req: AuthenticatedMedusaRequest<typeof UpsertProductBuilderSchema>,
  res: MedusaResponse
) => {
  const { result } = await upsertProductBuilderWorkflow(req.scope)
    .run({
      input: {
        product_id: req.params.id,
        ...req.validatedBody,
      },
    })

  res.json({
    product_builder: result.product_builder,
  })
}
```

First, you define a [Zod](https://zod.dev/) schema that represents the accepted request body. It includes optional custom fields, complementary products, and addon products.

Then, you export a `POST` route handler function, which will expose a `POST` API route at `/admin/products/[id]/builder`.

In the route handler, you execute the `upsertProductBuilderWorkflow` passing it the Medusa container, which is available in the `req.scope` property, and executing its `run` method.

You return the product builder in the response.

You'll test this API route later when you customize the Medusa Admin dashboard.

#### Add Validation Middleware

To validate the body parameters of requests sent to the API route, you need to apply a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

To apply a middleware to a route, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" highlights={middlewareHighlights}
import { 
  defineMiddlewares,
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { UpsertProductBuilderSchema } from "./admin/products/[id]/builder/route"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/admin/products/:id/builder",
      methods: ["POST"],
      middlewares: [
        validateAndTransformBody(UpsertProductBuilderSchema),
      ],
    },
  ],
})
```

You apply the `validateAndTransformBody` middleware to the `POST` route of the `/admin/products/:id/builder` path, passing it the Zod schema you created in the route file.

Any request that doesn't conform to the schema will receive a 400 Bad Request response.

Refer to the [Middlewares](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) documentation to learn more.

***

## Step 5: Retrieve Product Builder Data API Routes

In this step, you'll create API routes that retrieve data useful for your admin customizations later. You'll implement API routes to:

- Retrieve a product's builder configuration.
- Retrieve products that can be added as complementary products.
- Retrieve products that can be added as addon products.

### a. Retrieve Product Builder Configuration API Route

The first route you'll create is for retrieving a product's builder configuration.

You'll make the API route available at the `/admin/products/:id/builder` path. So, add the following in the same `src/api/admin/products/[id]/builder/route.ts` file:

```ts title="src/api/admin/products/[id]/builder/route.ts" highlights={getProductBuilderHighlights}
export const GET = async (
  req: AuthenticatedMedusaRequest<{ id: string }>,
  res: MedusaResponse
) => {
  const query = req.scope.resolve("query")
  
  const { data: productBuilders } = await query.graph({
    entity: "product_builder",
    fields: [
      "id",
      "product_id", 
      "custom_fields.*",
      "complementary_products.*",
      "complementary_products.product.*",
      "addons.*",
      "addons.product.*",
      "created_at",
      "updated_at",
    ],
    filters: {
      product_id: req.params.id,
    },
  })

  if (productBuilders.length === 0) {
    return res.status(404).json({
      message: `Product builder configuration not found for product ID: ${req.params.id}`,
    })
  }

  res.json({
    product_builder: productBuilders[0],
  })
}
```

Since you export a `GET` route handler function, you expose a `GET` API route at `/admin/products/:id/builder`.

In the route handler function, you resolve [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve the product builder configuration for the specified product ID. You also retrieve its custom fields, complementary products, and addon products.

You return the product builder configuration in the response.

### b. Retrieve Complementary Products API Route

Next, you'll create an API route that retrieves products that can be added as complementary products for another product. This is useful to allow the admin users to select complementary products when configuring a product builder.

To create the API route, create the file `src/api/admin/products/complementary/route.ts` with the following content:

```ts title="src/api/admin/products/complementary/route.ts" highlights={getComplementaryProductsRouteHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
import { createFindParams } from "@medusajs/medusa/api/utils/validators"
import { z } from "zod"

export const GetComplementaryProductsSchema = z.object({
  exclude_product_id: z.string(),
}).merge(createFindParams())

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const {
    exclude_product_id,
  } = req.validatedQuery

  const query = req.scope.resolve("query")

  const {
    data: products,
    metadata,
  } = await query.graph({
    entity: "product",
    fields: [
      "*",
      "variants.*",
    ],
    filters: {
      id: {
        $ne: exclude_product_id as string,
      },
      tags: {
        $or: [
          {
            value: {
              $eq: null,
            },
          },
          {
            value: {
              $ne: "addon",
            },
          },
        ],
      },
      status: "published",
    },
    pagination: req.queryConfig.pagination,
  })

  res.json({
    products,
    limit: metadata?.take,
    offset: metadata?.skip,
    count: metadata?.count,
  })
}
```

You define a Zod schema that requires passing a `exclude_product_id` query parameter to filter out the current product from the list of complementary products. You merge the schema with the `createFindParams` schema to include pagination and sorting parameters.

In the `GET` route handler, you retrieve the potential complementary products using Query. You apply the following filters on the products:

1. Exclude the current product from the list by filtering out the `exclude_product_id`.
2. Exclude products that have the "addon" tag, as these can only be sold as addons.
3. Exclude products that are not published.

You also apply pagination configurations using the `req.queryConfig.pagination` property. You'll learn how you can set these configurations in a bit.

Finally, you return the list of products in the response with pagination metadata.

#### Apply Query Validation and Configuration Middleware

Next, you'll apply a middleware to validate the query parameters and apply pagination configurations to the API route.

In `src/api/middlewares.ts`, add the following imports at the top of the file:

```ts title="src/api/middlewares.ts"
import { 
  validateAndTransformQuery,
} from "@medusajs/framework/http"
import { GetComplementaryProductsSchema } from "./admin/products/complementary/route"
```

Then, add a new route object in `defineMiddlewares`:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/products/complementary",
      methods: ["GET"],
      middlewares: [
        validateAndTransformQuery(GetComplementaryProductsSchema, {
          isList: true,
        }),
      ],
    },
  ],
})
```

You apply the `validateAndTransformQuery` middleware to the `GET` API route at `/admin/products/complementary`. The middleware accepts two parameters:

1. The Zod schema to validate the query parameters.
2. An object of [Request Query Configurations](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#request-query-configurations/index.html.md). You enable the `isList` option to indicate that the pagination query parameters should be added as query configurations in the `req.queryConfig.pagination` object.

### c. Retrieve Addon Products API Route

Finally, you'll create an API route that retrieves products that can be added as addon products for another product. This is useful to allow the admin users to select addon products when configuring a product builder.

To create the API route, create the file `src/api/admin/products/addons/route.ts` with the following content:

```ts title="src/api/admin/products/addons/route.ts" highlights={getAddonProductsHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve("query")

  const {
    data: products,
    metadata,
  } = await query.graph({
    entity: "product",
    fields: [
      "*",
      "variants.*",
    ],
    filters: {
      tags: {
        value: "addon",
      },
      status: "published",
    },
    pagination: req.queryConfig.pagination,
  })

  res.json({
    products,
    limit: metadata?.take,
    offset: metadata?.skip,
    count: metadata?.count,
  })
}
```

In the `GET` API route at `/admin/products/addons`, you retrieve the products that have the "addon" tag and are published. You return the products in the response with pagination data.

#### Apply Query Configuration Middleware

Since the API route should accept pagination query parameters, you need to apply the `validateAndTransformQuery` middleware to it.

In `src/api/middlewares.ts`, add the following import at the top of the file:

```ts title="src/api/middlewares.ts"
import { createFindParams } from "@medusajs/medusa/api/utils/validators"
```

Then, add a new route object in `defineMiddlewares`:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/products/addons",
      methods: ["GET"],
      middlewares: [
        validateAndTransformQuery(createFindParams(), {
          isList: true,
        }),
      ],
    },
  ],
})
```

You apply the `validateAndTransformQuery` on the route to allow passing pagination query parameters, and enabling `isList` to populate the `req.queryConfig.pagination` object.

You'll test out all of these routes in the next step.

***

## Step 6: Add Admin Widget in Product Details Page

In this step, you'll customize the Medusa Admin to allow admin users to manage a product's builder configurations.

The Medusa Admin dashboard is customizable, allowing you to insert widgets into existing pages, or create new pages.

Refer to the [Admin Development](https://docs.medusajs.com/docs/learn/fundamentals/admin/index.html.md) documentation to learn more.

In this step, you'll create the components to manage a product's builder configurations, then inject a widget into the product details page to show the configurations and allow managing them.

### a. Initialize JS SDK

To send requests to the Medusa server, you'll use the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md). It's already installed in your Medusa project, but you need to initialize it before using it in your customizations.

Create the file `src/admin/lib/sdk.ts` with the following content:

```ts title="src/admin/lib/sdk.ts" highlights={sdkHighlights}
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: process.env.MEDUSA_BACKEND_URL || "http://localhost:9000",
  debug: process.env.NODE_ENV === "development",
  auth: {
    type: "session",
  },
})
```

Learn more about the initialization options in the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) reference.

### b. Define Types

Next, you'll define types that you'll use in your admin customizations.

Create the file `src/admin/types.ts` with the following content:

```ts title="src/admin/types.ts" highlights={typesHighlights}
export type ProductBuilderBase = {
  id: string
  product_id: string
  created_at: string
  updated_at: string
}

export type CustomFieldBase = {
  id: string
  name: string
  type: "text" | "number"
  description?: string
  is_required: boolean
}

export type ComplementaryProductBase = {
  id: string
  product_id: string
  product?: {
    id: string
    title: string
  }
}

export type AddonProductBase = {
  id: string
  product_id: string
  product?: {
    id: string
    title: string
  }
}

// Product Builder API Response Types
export type ProductBuilderResponse = {
  product_builder: ProductBuilderBase & {
    custom_fields: CustomFieldBase[]
    complementary_products: ComplementaryProductBase[]
    addons: AddonProductBase[]
  }
}

// Form Data Types (for creating/updating)
export type CustomField = {
  id?: string
  name: string
  type: "text" | "number"
  description?: string
  is_required: boolean
}

export type ComplementaryProduct = {
  id?: string
  product_id: string
  product?: {
    id: string
    title: string
  }
}

export type AddonProduct = {
  id?: string
  product_id: string
  product?: {
    id: string
    title: string
  }
}
```

You define the following types:

- `ProductBuilderBase`: Base type for product builder configuration.
- `CustomFieldBase`: Base type for custom fields.
- `ComplementaryProductBase`: Base type for complementary products.
- `AddonProductBase`: Base type for addon products.
- `ProductBuilderResponse`: API response type for product builder configurations.
- `CustomField`: Type of custom fields in the form that creates or updates product builder configurations.
- `ComplementaryProduct`: Type of complementary products in the form that creates or updates product builder configurations.
- `AddonProduct`: Type of addon products in the form that creates or updates product builder configurations.

### c. Custom Fields Tab Component

To manage a product's builder configurations, you'll show a modal with tabs for custom fields, complementary products, and add-ons.

You'll start by creating the custom fields tab component, which allows admin users to manage custom fields for a product's builder configuration.

![Screenshot of how the custom fields tab will look like](https://res.cloudinary.com/dza7lstvk/image/upload/v1755089825/Medusa%20Resources/CleanShot_2025-08-13_at_15.56.15_2x_uwej4x.png)

To create the component, create the file `src/admin/components/custom-fields-tab.tsx` with the following content:

```tsx title="src/admin/components/custom-fields-tab.tsx" collapsibleLines="1-12" expandButtonLabel="Show Imports"
import {
  Button,
  Heading,
  Input,
  Label,
  Select,
  Checkbox,
  Text,
} from "@medusajs/ui"
import { Trash } from "@medusajs/icons"
import { CustomField } from "../types"

type CustomFieldsTabProps = {
  customFields: CustomField[]
  onCustomFieldsChange: (fields: CustomField[]) => void
}

export const CustomFieldsTab = ({
  customFields,
  onCustomFieldsChange,
}: CustomFieldsTabProps) => {
  const addCustomField = () => {
    const newFields = [
      ...customFields,
      {
        name: "",
        type: "text" as const,
        description: "",
        is_required: false,
      },
    ]
    onCustomFieldsChange(newFields)
  }

  const updateCustomField = (index: number, field: Partial<CustomField>) => {
    const updated = [...customFields]
    updated[index] = { ...updated[index], ...field }
    onCustomFieldsChange(updated)
  }

  const removeCustomField = (index: number) => {
    const filtered = customFields.filter((_, i) => i !== index)
    onCustomFieldsChange(filtered)
  }

  return (
    <div className="flex-1 overflow-y-auto p-6">
      <div className="space-y-4">
        <div className="flex items-center justify-between">
          <Heading level="h2">Custom Fields</Heading>
          <Button size="small" variant="secondary" onClick={addCustomField}>
            Add Field
          </Button>
        </div>
        
        {customFields.length === 0 ? (
          <Text className="text-ui-fg-muted">No custom fields configured.</Text>
        ) : (
          <div className="space-y-4">
            {customFields.map((field, index) => (
              <div key={index} className="p-4 border rounded-lg space-y-3">
                <div className="flex items-center justify-between">
                  <Label>Field {index + 1}</Label>
                  <Button 
                    size="small" 
                    variant="transparent" 
                    onClick={() => removeCustomField(index)}
                  >
                    <Trash />
                  </Button>
                </div>
                <div className="grid grid-cols-2 gap-3">
                  <div>
                    <Label>Name</Label>
                    <Input
                      value={field.name}
                      onChange={(e) => updateCustomField(index, { name: e.target.value })}
                      placeholder="Field name"
                    />
                  </div>
                  <div>
                    <Label>Type</Label>
                    <Select
                      value={field.type}
                      onValueChange={(value) => updateCustomField(index, { type: value as "text" | "number" })}
                    >
                      <Select.Trigger>
                        <Select.Value />
                      </Select.Trigger>
                      <Select.Content>
                        <Select.Item value="text">Text</Select.Item>
                        <Select.Item value="number">Number</Select.Item>
                      </Select.Content>
                    </Select>
                  </div>
                </div>
                <div>
                  <Label>Description (optional)</Label>
                  <Input
                    value={field.description || ""}
                    onChange={(e) => updateCustomField(index, { description: e.target.value })}
                    placeholder="Provide helpful instructions for this field"
                  />
                </div>
                <div className="flex items-center space-x-2">
                  <Checkbox
                    id={`required-${index}`}
                    checked={field.is_required}
                    onCheckedChange={(checked) => 
                      updateCustomField(index, { is_required: !!checked })
                    }
                  />
                  <Label htmlFor={`required-${index}`}>Required field</Label>
                </div>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  )
}
```

This component receives the custom fields and a function to change them as an input.

In the component, you show each custom field in its own section with fields for the name, type, description, and whether it's required.

You also add buttons to add a new custom field or remove an existing one.

When the admin user changes values of a custom field, creates a custom field, or deletes a custom field, you use the `onCustomFieldChange` callback to set the updated custom fields.

### d. Complementary Products Tab Component

Next, you'll create the complementary products tab component, which allows admin users to manage the complementary products for a product's builder configuration.

![Screenshot of how the complementary products tab will look like](https://res.cloudinary.com/dza7lstvk/image/upload/v1755089830/Medusa%20Resources/CleanShot_2025-08-13_at_15.56.20_2x_jxix2n.png)

Create the file `src/admin/components/complementary-products-tab.tsx` with the following content:

```tsx title="src/admin/components/complementary-products-tab.tsx" collapsibleLines="1-13" expandButtonLabel="Show Imports"
import { AdminProduct } from "@medusajs/framework/types"
import { 
  Heading, 
  Checkbox, 
  createDataTableColumnHelper,
  DataTable,
  DataTablePaginationState,
  useDataTable,
} from "@medusajs/ui"
import { useState } from "react"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"
import { ComplementaryProduct } from "../types"

type ComplementaryProductsTabProps = {
  product: AdminProduct
  complementaryProducts: ComplementaryProduct[]
  onComplementaryProductSelection: (productId: string, checked: boolean) => void
}

type ProductRow = {
  id: string
  title: string
  status: string
}

const columnHelper = createDataTableColumnHelper<ProductRow>()

export const ComplementaryProductsTab = ({
  product,
  complementaryProducts,
  onComplementaryProductSelection,
}: ComplementaryProductsTabProps) => {
  const [pagination, setPagination] = useState<DataTablePaginationState>({
    pageIndex: 0,
    pageSize: 20,
  })

  // Fetch products for selection with pagination
  const { data: productsData, isLoading } = useQuery({
    queryKey: ["products", "complementary", pagination],
    queryFn: async () => {
      const query = new URLSearchParams({
        limit: pagination.pageSize.toString(),
        offset: (pagination.pageIndex * pagination.pageSize).toString(),
        exclude_product_id: product.id,
      })
      const response: any = await sdk.client.fetch(
        `/admin/products/complementary?${query.toString()}`
      )
      return {
        products: response.products,
        count: response.count,
      }
    },
  })

  const columns = [
    columnHelper.display({
      id: "select",
      header: "Select",
      cell: ({ row }) => {
        const isChecked = !!complementaryProducts.find(
          (cp) => cp.product_id === row.original.id
        )
        return (
          <Checkbox
            checked={isChecked}
            onCheckedChange={(checked) => 
              onComplementaryProductSelection(row.original.id, !!checked)
            }
            className="my-2"
          />
        )
      },
    }),
    columnHelper.accessor("title", {
      header: "Product",
    }),
  ]

  const table = useDataTable({
    data: productsData?.products || [],
    columns,
    rowCount: productsData?.count || 0,
    getRowId: (row) => row.id,
    isLoading,
    pagination: {
      state: pagination,
      onPaginationChange: setPagination,
    },
  })

  return (
    <div>
      <DataTable instance={table}>
        <DataTable.Toolbar>
          <Heading level="h2">Complementary Products</Heading>
        </DataTable.Toolbar>
        <DataTable.Table />
        <DataTable.Pagination />
      </DataTable>
    </div>
  )
}
```

This component receives the following props:

- `product`: The main product being configured.
- `complementaryProducts`: A set of selected complementary product IDs.
- `onComplementaryProductSelection`: A function to handle selection changes.

In the component, you retrieve the products using the [retrieve complementary products API route](#b-retrieve-complementary-products-api-route) you created. You show these products in a table with a checkbox for selection.

When a product is selected or de-selected, you use the `onComplementaryProductSelection` function to update the list of selected complementary products.

You use [Tanstack Query](https://tanstack.com/query/latest) to send requests with the JS SDK, which simplifies data fetching and caching.

### e. Addon Products Tab Component

Next, you'll create the last tab of the product builder configuration modal. It will allow the admin user to select addons of the product.

![Screenshot of how the addons tab will look like](https://res.cloudinary.com/dza7lstvk/image/upload/v1755089831/Medusa%20Resources/CleanShot_2025-08-13_at_15.56.24_2x_zmryk1.png)

To create the component, create the file `src/admin/components/addons-tab.tsx` with the following content:

```tsx title="src/admin/components/addons-tab.tsx" collapsibleLines="1-12" expandButtonLabel="Show Imports"
import { 
  Heading, 
  Checkbox, 
  createDataTableColumnHelper,
  DataTable,
  DataTablePaginationState,
  useDataTable,
} from "@medusajs/ui"
import { useState } from "react"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"
import { AddonProduct } from "../types"

type AddonsTabProps = {
  addonProducts: AddonProduct[]
  onAddonProductSelection: (productId: string, checked: boolean) => void
}

type ProductRow = {
  id: string
  title: string
  status: string
}

const columnHelper = createDataTableColumnHelper<ProductRow>()

export const AddonsTab = ({
  addonProducts,
  onAddonProductSelection,
}: AddonsTabProps) => {
  const [pagination, setPagination] = useState<DataTablePaginationState>({
    pageIndex: 0,
    pageSize: 20,
  })

  // Fetch addon products with pagination
  const { data: addonsData, isLoading } = useQuery({
    queryKey: ["products", "addon", pagination],
    queryFn: async () => {
      const response: any = await sdk.client.fetch(
        `/admin/products/addons?limit=${pagination.pageSize}&offset=${pagination.pageIndex * pagination.pageSize}`
      )
      return {
        addons: response.products || [],
        count: response.count || 0,
      }
    },
  })

  const columns = [
    columnHelper.display({
      id: "select",
      header: "Select",
      cell: ({ row }) => {
        const isChecked = !!addonProducts.find(
          (ap) => ap.product_id === row.original.id
        )
        return (
          <Checkbox
            checked={isChecked}
            onCheckedChange={(checked) => 
              onAddonProductSelection(row.original.id, !!checked)
            }
            className="my-2"
          />
        )
      },
    }),
    columnHelper.accessor("title", {
      header: "Product",
    }),
  ]

  const tableData = addonsData?.addons || []

  const table = useDataTable({
    data: tableData,
    columns,
    rowCount: addonsData?.count || 0,
    getRowId: (row) => row.id,
    isLoading,
    pagination: {
      state: pagination,
      onPaginationChange: setPagination,
    },
  })

  return (
    <div>
      <DataTable instance={table}>
        <DataTable.Toolbar>
          <Heading level="h2">Addon Products</Heading>
        </DataTable.Toolbar>
        <DataTable.Table />
        <DataTable.Pagination />
      </DataTable>
    </div>
  )
}
```

The component receives the following props:

- `addonProducts`: A set of selected addon product IDs.
- `onAddonProductSelection`: A callback function to handle addon product selection changes.

In the component, you retrieve the products using the [retrieve addon products API route](#c-retrieve-addon-products-api-route) you created. You show these products in a table with a checkbox for selection.

When a product is selected or de-selected, you use the `onAddonProductSelection` function to update the list of selected addon products.

### d. Product Builder Configurations Modal

Now that you have the components for managing custom fields, complementary products, and add-ons, you'll create a modal component that wraps these tabs in a modal.

Create the file `src/admin/components/product-builder-modal.tsx` with the following content:

```tsx title="src/admin/components/product-builder-modal.tsx" collapsibleLines="1-21" expandButtonLabel="Show Imports"
import {
  Button,
  FocusModal,
  Heading,
  toast,
  ProgressTabs,
} from "@medusajs/ui"
import { useState, useEffect } from "react"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"
import { 
  CustomField, 
  ComplementaryProduct,
  ProductBuilderResponse,
  AddonProduct,
} from "../types"
import { AdminProduct } from "@medusajs/framework/types"
import { ComplementaryProductsTab } from "./complementary-products-tab"
import { AddonsTab } from "./addons-tab"
import { CustomFieldsTab } from "./custom-fields-tab"

type ProductBuilderModalProps = {
  open: boolean
  onOpenChange: (open: boolean) => void
  product: AdminProduct
  initialData?: ProductBuilderResponse["product_builder"]
  onSuccess: () => void
}

export const ProductBuilderModal = ({
  open,
  onOpenChange,
  product,
  initialData,
  onSuccess,
}: ProductBuilderModalProps) => {
  const [customFields, setCustomFields] = useState<CustomField[]>([])
  const [complementaryProducts, setComplementaryProducts] = useState<ComplementaryProduct[]>([])
  const [addonProducts, setAddonProducts] = useState<AddonProduct[]>([])
  const [currentTab, setCurrentTab] = useState("custom-fields")

  const queryClient = useQueryClient()

  // Helper function to determine tab status
  const getTabStatus = (tabName: string): "not-started" | "in-progress" | "completed" => {
    const isCurrentTab = currentTab === tabName
    switch (tabName) {
      case "custom-fields":
        return customFields.length > 0 ? isCurrentTab ? 
          "in-progress" : "completed" :
            "not-started"
      case "complementary":
        return complementaryProducts.length > 0 ? isCurrentTab ? 
          "in-progress" : "completed" :
            "not-started"
      case "addons":
        return addonProducts.length > 0 ? isCurrentTab ? 
          "in-progress" : "completed" :
            "not-started"
      default:
        return "not-started"
    }
  }

  // Load initial data when modal opens
  useEffect(() => {
    setCustomFields(initialData?.custom_fields || [])
    setComplementaryProducts(initialData?.complementary_products || [])
    setAddonProducts(initialData?.addons || [])
    
    // Reset to first tab when modal opens
    setCurrentTab("custom-fields")
  }, [open, initialData])

  const { mutateAsync: saveConfiguration, isPending: isSaving } = useMutation({
    mutationFn: async (data: any) => {
      return await sdk.client.fetch(`/admin/products/${product.id}/builder`, {
        method: "POST",
        body: data,
      })
    },
    onSuccess: () => {
      toast.success("Builder configuration saved successfully")
      queryClient.invalidateQueries({
        queryKey: ["product-builder", product.id],
      })
      onSuccess()
    },
    onError: (error: any) => {
      toast.error(`Failed to save configuration: ${error.message}`)
    },
  })

  const handleSave = async () => {
    try {
      await saveConfiguration({
        custom_fields: customFields,
        complementary_products: complementaryProducts.map((cp) => ({
          id: cp.id,
          product_id: cp.product_id,
        })),
        addon_products: addonProducts.map((ap) => ({
          id: ap.id,
          product_id: ap.product_id,
        })),
      })
    } catch (error) {
      toast.error(`Error saving configuration: ${error instanceof Error ? error.message : "Unknown error"}`)
    }
  }

  const handleComplementarySelection = (productId: string, checked: boolean) => {
    setComplementaryProducts((prev) => {
      if (checked) {
        return [
          ...prev,
          {
            product_id: productId,
          },
        ]
      }

      return prev.filter((cp) => cp.product_id !== productId)
    })
  }

  const handleAddonSelection = (productId: string, checked: boolean) => {
    setAddonProducts((prev) => {
      if (checked) {
        return [
          ...prev,
          {
            product_id: productId,
          },
        ]
      }

      return prev.filter((ap) => ap.product_id !== productId)
    })
  }

  const handleNextTab = () => {
    if (currentTab === "custom-fields") {
      setCurrentTab("complementary")
    } else if (currentTab === "complementary") {
      setCurrentTab("addons")
    }
  }

  const isLastTab = currentTab === "addons"

  // TODO render modal
}
```

The `ProductBuilderModal` accepts the following props:

- `open`: Whether the modal is open.
- `onOpenChange`: Function to change the open state.
- `product`: The product being configured.
- `initialData`: The initial data for the product builder.
- `onSuccess`: Function to execute when the configuration is saved successfully.

In the component, you define the following variables and functions:

- `customFields`: Stores the custom fields entered by the admin.
- `complementaryProducts`: Stores the complementary products selected by the admin.
- `addonProducts`: Stores the addon products selected by the admin.
- `currentTab`: The current active tab.
- `queryClient`: The Tanstack Query client which is useful to refetch data.
- `getTabStatus`: A function to get the status of each tab.
- `saveConfiguration`: A mutation to save the configuration when the admin submits them.
- `handleSave`: A function that executes the mutation to save the configurations.
- `handleComplementarySelection`: A function to update the selected complementary products.
- `handleAddonSelection`: A function to update the selected addon products.
- `handleNextTab`: A function to open the next tab.
- `isLastTab`: A boolean indicating if the current tab is the last tab.

Next, to render the form, replace the `TODO` in the component with the following:

```tsx title="src/admin/components/product-builder-modal.tsx"
return (
  <FocusModal open={open} onOpenChange={onOpenChange}>
    <FocusModal.Content>
      <FocusModal.Header>
        <Heading level="h1">Builder Configuration</Heading>
      </FocusModal.Header>
      <FocusModal.Body className="flex flex-1 flex-col overflow-hidden">
        <ProgressTabs value={currentTab} onValueChange={setCurrentTab} className="flex flex-1 flex-col">
          <ProgressTabs.List className="flex items-center border-b">
            <ProgressTabs.Trigger 
              value="custom-fields" 
              status={getTabStatus("custom-fields")}
            >
              Custom Fields
            </ProgressTabs.Trigger>
            <ProgressTabs.Trigger 
              value="complementary" 
              status={getTabStatus("complementary")}
            >
              Complementary Products
            </ProgressTabs.Trigger>
            <ProgressTabs.Trigger 
              value="addons" 
              status={getTabStatus("addons")}
            >
              Addon Products
            </ProgressTabs.Trigger>
          </ProgressTabs.List>

          <ProgressTabs.Content value="custom-fields" className="flex-1 overflow-hidden">
            <CustomFieldsTab
              customFields={customFields}
              onCustomFieldsChange={setCustomFields}
            />
          </ProgressTabs.Content>

          <ProgressTabs.Content value="complementary" className="flex-1 overflow-hidden">
            <ComplementaryProductsTab
              product={product}
              complementaryProducts={complementaryProducts}
              onComplementaryProductSelection={handleComplementarySelection}
            />
          </ProgressTabs.Content>

          <ProgressTabs.Content value="addons" className="flex-1 overflow-hidden">
            <AddonsTab
              addonProducts={addonProducts}
              onAddonProductSelection={handleAddonSelection}
            />
          </ProgressTabs.Content>
        </ProgressTabs>
      </FocusModal.Body>
      <FocusModal.Footer>
        <div className="flex items-center justify-between">
          <div className="flex items-center gap-x-2">
            <Button variant="secondary" onClick={() => onOpenChange(false)}>
              Cancel
            </Button>
            <Button
              variant="primary"
              onClick={isLastTab ? handleSave : handleNextTab}
              isLoading={isLastTab && isSaving}
            >
              {isLastTab ? "Save Configuration" : "Next"}
            </Button>
          </div>
        </div>
      </FocusModal.Footer>
    </FocusModal.Content>
  </FocusModal>
)
```

You display a [Focus Modal](https://docs.medusajs.com/ui/components/focus-modal/index.html.md) that shows the tabs with each of their content.

The modal has a button to move between tabs, then save the changes when the admin user reaches the last tab.

### e. Add Widget to Product Details Page

Finally, you'll create the widget that will be injected to the product details page.

Create the file `src/admin/widgets/product-builder-widget.tsx` with the following content:

```tsx title="src/admin/widgets/product-builder-widget.tsx" collapsibleLines="1-12" expandButtonLabel="Show Imports"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading, Text, Button } from "@medusajs/ui"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"
import { 
  DetailWidgetProps, 
  AdminProduct,
} from "@medusajs/framework/types"
import { useState } from "react"
import { ProductBuilderModal } from "../components/product-builder-modal"
import { ProductBuilderResponse } from "../types"

const ProductBuilderWidget = ({ 
  data: product,
}: DetailWidgetProps<AdminProduct>) => {
  const [modalOpen, setModalOpen] = useState(false)

  const { data, isLoading, refetch } = useQuery<ProductBuilderResponse>({
    queryFn: () => sdk.client.fetch(`/admin/products/${product.id}/builder`),
    queryKey: ["product-builder", product.id],
    retry: false,
  })

  const formatSummary = (items: any[], getTitle: (item: any) => string) => {
    if (!items || items.length === 0) {return "-"}
    if (items.length === 1) {return getTitle(items[0])}
    return `${getTitle(items[0])} + ${items.length - 1} more`
  }

  const customFieldsSummary = formatSummary(
    data?.product_builder?.custom_fields || [],
    (field) => field.name
  )

  const complementaryProductsSummary = formatSummary(
    data?.product_builder?.complementary_products || [],
    (item) => item.product?.title || "Unnamed Product"
  )

  const addonsSummary = formatSummary(
    data?.product_builder?.addons || [],
    (item) => item.product?.title || "Unnamed Product"
  )

  return (
    <>
      <Container className="divide-y p-0">
        <div className="flex items-center justify-between px-6 py-4">
          <Heading level="h2">Builder Configuration</Heading>
          <Button
            size="small"
            variant="secondary"
            onClick={() => setModalOpen(true)}
          >
            Edit
          </Button>
        </div>
        <div>
          {isLoading ? (
            <Text>Loading...</Text>
          ) : (
            <>
              <div
                className={
                  "text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4 border-b"
                }
              >
                <Text size="small" weight="plus" leading="compact">
                  Custom Fields
                </Text>

                <Text
                  size="small"
                  leading="compact"
                  className="whitespace-pre-line text-pretty"
                >
                  {customFieldsSummary}
                </Text>
              </div>
              <div
                className={
                  "text-ui-fg-subtle grid grid-cols-2 px-6 py-4 items-center border-b"
                }
              >
                <Text size="small" weight="plus" leading="compact">
                  Complementary Products
                </Text>

                <Text
                  size="small"
                  leading="compact"
                  className="whitespace-pre-line text-pretty"
                >
                  {complementaryProductsSummary}
                </Text>
              </div>
              <div
                className={
                  "text-ui-fg-subtle grid grid-cols-2 px-6 py-4 items-center"
                }
              >
                <Text size="small" weight="plus" leading="compact">
                  Addon Products
                </Text>

                <Text
                  size="small"
                  leading="compact"
                  className="whitespace-pre-line text-pretty"
                >
                  {addonsSummary}
                </Text>
              </div>
            </>
          )}
        </div>
      </Container>

      <ProductBuilderModal
        open={modalOpen}
        onOpenChange={setModalOpen}
        product={product}
        initialData={data?.product_builder}
        onSuccess={() => {
          refetch()
          setModalOpen(false)
        }}
      />
    </>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.side.after",
})

export default ProductBuilderWidget
```

A widget file must export:

- A default React component. This component renders the widget's UI.
- A `config` object created with `defineWidgetConfig` from the Admin SDK. It accepts an object with the `zone` property that indicates where the widget will be rendered in the Medusa Admin dashboard.

In the widget's component, you retrieve the product builder configuration of the current product, if available. Then, you show a summary of the configurations, with a button to edit the configurations.

When the edit button is clicked, the product builder modal is shown with the tabs for custom fields, complementary products, and addon products.

### Test the Admin Widget

To test out the admin widget for product builder configurations:

1. Start the Medusa application with the following command:

```bash npm2yarn
npm run dev
```

2. Open the Medusa Admin dashboard at `localhost:9000/app` and login.
3. Go to Settings -> Product Tags.
4. Create a tag with the value `addon`.
5. Go back to the Products page, and choose an existing product to mark as an addon.
6. Change the product's tag from the Organize section.
7. Go back to the Products page, and choose an existing product to manage its builder configurations.
8. Scroll down to the end of the product's details page. You'll find a new "Builder Configuration" section. This is the widget you inserted.

![Builder configuration widget in the product details page](https://res.cloudinary.com/dza7lstvk/image/upload/v1755096182/Medusa%20Resources/CleanShot_2025-08-13_at_17.30.06_2x_hnfsdl.png)

9. Click on the Edit button to edit the configurations.
10. Add custom fields such as engravings, select complementary products such as keyboard, and add add-ons like a warranty.
11. Once you're done, click on the "Save Configuration" button. The modal will be closed and you can see the updated configurations in the widget.

![Updated builder configuration data showing in the widget](https://res.cloudinary.com/dza7lstvk/image/upload/v1755096296/Medusa%20Resources/CleanShot_2025-08-13_at_17.44.45_2x_zqonoy.png)

***

## Step 7: Customize Product Page on Storefront

In this step, you'll customize the product details page on the storefront to show the product builder configurations.

Alongside the variant options like color and size, which are already available in Medusa, you'll show:

- The custom fields, allowing the customer to enter their values.
- The complementary products, allowing the customer to add them to the cart alongside the main product.
- The addon products, allowing the customer to add them to the cart as part of the main product.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-product-builder`, you can find the storefront by going back to the parent directory and changing to the `medusa-product-builder-storefront` directory:

```bash
cd ../medusa-product-builder-storefront # change based on your project name
```

### a. Define Types for Product Builder Configurations

You'll start by defining types that you'll use in your storefront customizations.

In `src/types/global.ts`, add the following import at the top of the file:

```ts title="src/types/global.ts" badgeLabel="Storefront" badgeColor="blue"
import { 
  StoreProduct,
} from "@medusajs/types"
```

Then, add the following type definitions at the end of the file:

```ts title="src/types/global.ts" highlights={storefrontTypesHighlights} badgeLabel="Storefront" badgeColor="blue"
export type ProductBuilderCustomField = {
  id: string
  name: string
  type: "text" | "number"
  description?: string
  is_required: boolean
}

export type ProductBuilderComplementaryProduct = {
  id: string
  product: StoreProduct
}

export type ProductBuilderAddon = {
  id: string
  product: StoreProduct
}

export type ProductBuilder = {
  id: string
  product_id: string
  custom_fields: ProductBuilderCustomField[]
  complementary_products: ProductBuilderComplementaryProduct[]
  addons: ProductBuilderAddon[]
}

// Extended Product Type with Product Builder
export type ProductWithBuilder = StoreProduct & {
  product_builder?: ProductBuilder
}

// Product Builder Configuration Types
export type CustomFieldValue = {
  field_id: string
  value: string | number
}

export type ComplementarySelection = {
  product_id: string
  variant_id: string
  title: string
  thumbnail?: string
  price: number
}

export type AddonSelection = {
  product_id: string
  variant_id: string
  title: string
  thumbnail?: string
  price: number
  quantity: number
}

export type BuilderConfiguration = {
  custom_fields: CustomFieldValue[]
  complementary_products: ComplementarySelection[]
  addons: AddonSelection[]
}
```

You define the following types:

- `ProductBuilderCustomField`: A custom field in a product builder configuration.
- `ProductBuilderComplementaryProduct`: A complementary product in a product builder configuration.
- `ProductBuilderAddon`: An add-on product in a product builder configuration.
- `ProductBuilder`: The main product builder configuration object.
- `ProductWithBuilder`: A Medusa product with an associated product builder configuration.
- `CustomFieldValue`: A value entered by the customer for a custom field.
- `ComplementarySelection`: A selected complementary product in a product builder configuration.
- `AddonSelection`: A selected add-on product in a product builder configuration.
- `BuilderConfiguration`: The overall builder configuration chosen by the customer for a product.

### b. Retrieve Product Builder Configuration

Next, you need to retrieve the builder configuration for a product when the customer views its details page.

Since you've defined a link between the product and its builder configuration, you can retrieve the builder configuration of a product by specifying it in the `fields` query parameter of the [List Products API Route](https://docs.medusajs.com/api/store#products_getproducts).

In `src/lib/data/products.ts`, add the following import at the top of the file:

```ts title="src/lib/data/products.ts" badgeLabel="Storefront" badgeColor="blue"
import { ProductWithBuilder } from "../../types/global"
```

Then, change the return type of the `listProducts` function:

```ts title="src/lib/data/products.ts" badgeLabel="Storefront" badgeColor="blue" highlights={[["6"]]}
export const listProducts = async ({
  // ...
}: {
  // ...
}): Promise<{
  response: { products: ProductWithBuilder[]; count: number }
  // ...
}> => {
  // ...
}
```

Next, find the `sdk.client.fetch` call inside the `listProducts` function and change its type argument:

```ts title="src/lib/data/products.ts" badgeLabel="Storefront" badgeColor="blue" highlights={[["2"]]}
return sdk.client
  .fetch<{ products: ProductWithBuilder[]; count: number }>(
    // ...
  )
```

Next, find the `fields` query parameter and add to it the product builder data:

```ts title="src/lib/data/products.ts" badgeLabel="Storefront" badgeColor="blue" highlights={[["7"]]}
return sdk.client
  .fetch<{ products: ProductWithBuilder[]; count: number }>(
    `/store/products`,
    {
      query: {
        fields:
          "*variants.calculated_price,+variants.inventory_quantity,+metadata,+tags,*product_builder,*product_builder.custom_fields,*product_builder.complementary_products,*product_builder.complementary_products.product,*product_builder.complementary_products.product.variants,*product_builder.addons,*product_builder.addons.product,*product_builder.addons.product.variants",
        // ...
      },
      // ...
    }
  )
```

You retrieve for each product its builder configurations, its custom fields, complementary products, and add-on products. You also retrieve the product and variant details of the complementary and addon products.

Finally, change the return type of the `listProductsWithSort` to also include the product builder data:

```ts title="src/lib/data/products.ts" badgeLabel="Storefront" badgeColor="blue" highlights={[["6"]]}
export const listProductsWithSort = async ({
  // ...
}: {
  // ...
}): Promise<{
  response: { products: ProductWithBuilder[]; count: number }
  // ...
}> => {
  // ...
}
```

### c. Add Product Builder Configuration Utilities

Next, you'll add utility functions that are useful in your customizations.

Create the file `src/lib/util/product-builder.ts` with the following content:

```ts title="src/lib/util/product-builder.ts" badgeLabel="Storefront" badgeColor="blue" highlights={productBuilderUtilityHighlights}
import { ProductWithBuilder, ProductBuilder, LineItemWithBuilderMetadata } from "../../types/global"

// Utility function to check if a product has builder configuration
export const hasProductBuilder = (
  product: ProductWithBuilder
): product is ProductWithBuilder & { product_builder: ProductBuilder } => {
  return !!product.product_builder
}

// Utility function to check if a product has custom fields
export const hasCustomFields = (
  product: ProductWithBuilder
): boolean => {
  return hasProductBuilder(product) && product.product_builder.custom_fields.length > 0
}

// Utility function to check if a product has complementary products
export const hasComplementaryProducts = (
  product: ProductWithBuilder
): boolean => {
  return hasProductBuilder(product) && product.product_builder.complementary_products.length > 0
}

// Utility function to check if a product has addons
export const hasAddons = (
  product: ProductWithBuilder
): boolean => {
  return hasProductBuilder(product) && product.product_builder.addons.length > 0
}
```

You define the following utilities:

- `hasProductBuilder`: Checks if a product has a product builder configuration.
- `hasCustomFields`: Checks if a product has custom fields.
- `hasComplementaryProducts`: Checks if a product has complementary products.
- `hasAddons`: Checks if a product has addons.

### d. Implement Product Builder Configuration Component

In this section, you'll implement the component that will show the product builder configurations on the product details page. It will show inputs for custom fields, and variant selection for complementary and addon products.

![Screenshot of how the product builder configuration component will look like](https://res.cloudinary.com/dza7lstvk/image/upload/v1755098570/Medusa%20Resources/CleanShot_2025-08-13_at_18.22.29_2x_zdtjlk.png)

You'll first create the `VariantSelectionRow` component that you'll use to show the complementary and addon product variants.

Create the file `src/modules/products/components/product-builder-config/variant-selector.tsx` with the following content:

```tsx title="src/modules/products/components/product-builder-config/variant-selector.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={variantSelectorHighlights}
"use client"

import { HttpTypes } from "@medusajs/types"
import { 
  Badge, 
  Text, 
} from "@medusajs/ui"
import { getProductPrice } from "../../../../lib/util/get-product-price"

type VariantSelectionRowProps = {
  variant: HttpTypes.StoreProductVariant
  product: HttpTypes.StoreProduct
  isSelected: boolean
  isLoading: boolean
  onToggle: (productId: string, variantId: string, title: string, thumbnail: string | undefined, price: number) => void
}

const VariantSelector: React.FC<VariantSelectionRowProps> = ({
  variant,
  product,
  isSelected,
  isLoading,
  onToggle,
}) => {
  const {
    calculated_price: price = 0,
    calculated_price_number: priceNumber = 0,
  } = getProductPrice({
    product,
    variantId: variant.id,
  }).variantPrice || {}

  const inStock = !variant.manage_inventory || variant.allow_backorder || (
    variant.manage_inventory && (variant.inventory_quantity || 0) > 0
  )

  return (
    <button 
      key={variant.id}
      onClick={() => {
        if (inStock) {
          onToggle(
            product.id!,
            variant.id!,
            `${product.title} - ${variant.title}`,
            product.thumbnail || undefined,
            priceNumber
          )
        }
      }}
      disabled={!inStock}
      className={`
        border-ui-border-base bg-ui-bg-subtle border text-small-regular h-12 rounded-rounded p-2 w-full flex items-center justify-between
        ${!inStock 
          ? "opacity-50 cursor-not-allowed" 
          : isSelected 
          ? "border-ui-border-interactive" 
          : "hover:shadow-elevation-card-rest transition-shadow ease-in-out duration-150"
        }
      `}
    >
      <div className="flex items-center space-x-3">
        {product.thumbnail && (
          <div className="w-8 h-8 rounded overflow-hidden bg-ui-bg-subtle flex-shrink-0">
            <img
              src={product.thumbnail}
              alt={product.title || ""}
              className="w-full h-full object-cover"
            />
          </div>
        )}
        
        <div className="flex flex-col items-start">
          <div className="flex items-center gap-1">
            <Text className="text-sm font-medium text-ui-fg-base">
              {product.title}
            </Text>
          </div>
          <Text className={`text-xs ${!inStock ? "text-ui-fg-disabled" : "text-ui-fg-subtle"}`}>
            {variant.title}
            {!inStock && " (Out of Stock)"}
          </Text>
        </div>
      </div>
      
      <Badge size="small">
        {isLoading ? "..." : price}
      </Badge>
    </button>
  )
}

export default VariantSelector
```

This component accepts the following props:

- `variant`: The variant being displayed.
- `product`: The product that the variant belongs to.
- `isSelected`: Whether the variant is selected.
- `isLoading`: Whether the variant's price is currently being loaded.
- `onToggle`: A function to toggle the variant selection.

In the component, you show the variant's details and allow customers to toggle its selection.

Next, you'll create the component that will show the product builder.

Create the file `src/modules/products/components/product-builder-config/index.tsx` with the following content:

```tsx title="src/modules/products/components/product-builder-config/index.tsx" collapsibleLines="1-25" expandButtonLabel="Show Imports" badgeLabel="Storefront" badgeColor="blue" highlights={productBuilderConfigHighlights}
"use client"

import { useState, useEffect } from "react"
import { HttpTypes } from "@medusajs/types"
import { 
  Text, 
  Input, 
} from "@medusajs/ui"
import Divider from "@modules/common/components/divider"
import { 
  ProductWithBuilder, 
  BuilderConfiguration,
  CustomFieldValue,
  ComplementarySelection,
  AddonSelection,
} from "../../../../types/global"
import { 
  hasProductBuilder, 
  hasCustomFields, 
  hasComplementaryProducts, 
  hasAddons, 
} from "@lib/util/product-builder"
import { listProducts } from "@lib/data/products"
import VariantSelector from "./variant-selector"

type ProductBuilderConfigProps = {
  product: ProductWithBuilder
  countryCode: string
  onConfigurationChange: (config: BuilderConfiguration) => void
  onValidationChange: (isValid: boolean) => void
}

const ProductBuilderConfig: React.FC<ProductBuilderConfigProps> = ({
  product,
  countryCode,
  onConfigurationChange,
  onValidationChange,
}) => {
  // Configuration state
  const [customFields, setCustomFields] = useState<CustomFieldValue[]>([])
  const [complementaryProducts, setComplementaryProducts] = useState<ComplementarySelection[]>([])
  const [addons, setAddons] = useState<AddonSelection[]>([])

  // UI state
  const [isLoadingPrices, setIsLoadingPrices] = useState(false)
  const [productPrices, setProductPrices] = useState<Map<string, HttpTypes.StoreProduct>>(new Map())

  // Early return if no product builder
  if (!hasProductBuilder(product)) {
    return null
  }

  const builder = product.product_builder

  // Custom field handlers
  const handleCustomFieldChange = (fieldId: string, value: string | number) => {
    setCustomFields((prev) => {
      const existing = prev.find((f) => f.field_id === fieldId)
      if (existing) {
        return prev.map((f) => {
          return f.field_id === fieldId ? { ...f, value } : f
        })
      }
      return [...prev, { field_id: fieldId, value }]
    })
  }

  // Complementary product handlers
  const handleComplementaryToggle = (
    productId: string,
    variantId: string,
    title: string,
    thumbnail: string | undefined,
    price: number
  ) => {
    setComplementaryProducts((prev) => {
      const prevIndex = prev.findIndex((p) => p.variant_id === variantId)
      if (prevIndex !== -1) {
        return [...prev].splice(prevIndex, 1)
      }
      return [...prev, { product_id: productId, variant_id: variantId, title, thumbnail, price }]
    })
  }

  // Addon handlers
  const handleAddonToggle = (
    productId: string,
    variantId: string,
    title: string,
    thumbnail: string | undefined,
    price: number
  ) => {
    setAddons((prev) => {
      const prevIndex = prev.findIndex((p) => p.variant_id === variantId)
      if (prevIndex !== -1) {
        return [...prev].splice(prevIndex, 1)
      }
      return [...prev, { product_id: productId, variant_id: variantId, title, thumbnail, price, quantity: 1 }]
    })
  }

  const showCustomFields = hasCustomFields(product)
  const showComplementaryProducts = hasComplementaryProducts(product)
  const showAddons = hasAddons(product)

  // TODO add useEffect statements
}

export default ProductBuilderConfig
```

The `ProductBuilderConfig` component accepts the following props:

- `product`: The product being configured.
- `countryCode`: The country code for pricing and availability.
- `onConfigurationChange`: Callback for when the configuration changes.
- `onValidationChange`: Callback for when the validation state changes.

In the component, you define the following variables and functions:

- `customFields`: Stores custom fields' values.
- `complementaryProducts`: Stores selected complementary product details.
- `addons`: Stores selected addon product details.
- `isLoadingPrices`: Indicates if the product prices are being loaded.
- `productPrices`: Stores the loaded product prices.
- `builder`: The product builder configuration.
- `handleCustomFieldChange`: Updates the custom fields state.
- `handleComplementaryToggle`: Toggles the selection of complementary products.
- `handleAddonToggle`: Toggles the selection of addons.

Next, you'll add a `useEffect` statements that call the `onConfigurationChange` and `onValidationChange` callbacks when the configuration changes. Replace the `TODO` with the following:

```tsx title="src/modules/products/components/product-builder-config/index.tsx" badgeLabel="Storefront" badgeColor="blue"
// Update configuration when any field changes
useEffect(() => {
  onConfigurationChange({
    custom_fields: customFields,
    complementary_products: complementaryProducts,
    addons: addons,
  })
}, [customFields, complementaryProducts, addons, onConfigurationChange])

// Validate required fields and notify parent
useEffect(() => {
  // Check required custom fields
  const requiredCustomFields = builder.custom_fields.filter((field) => field.is_required)
  const customFieldsValid = requiredCustomFields.every((field) => {
    const fieldValue = customFields.find((cf) => cf.field_id === field.id)?.value
    return fieldValue !== undefined && fieldValue !== "" && fieldValue !== 0
  })

  onValidationChange(customFieldsValid)
}, [customFields, builder, onValidationChange])

// TODO add more useEffect statements
```

You add a `useEffect` call that triggers the `onConfigurationChange` callback when configurations are updated, and another that validates the custom fields and triggers the `onValidationChange` callback when the validation state changes.

You'll need one more `useEffect` statement that loads the prices of complementary and addon product variants. Replace the `TODO` with the following:

```tsx title="src/modules/products/components/product-builder-config/index.tsx" badgeLabel="Storefront" badgeColor="blue"
// Fetch product prices for complementary products and addons
useEffect(() => {
  const fetchProductPrices = async () => {
    const productIds = new Set<string>([
      ...builder.complementary_products.map((comp) => comp.product.id!),
      ...builder.addons.map((addon) => addon.product.id!),
    ])

    if (productIds.size === 0) {
      return
    }

    setIsLoadingPrices(true)
    
    try {
      // Fetch all products with their pricing information
      const { response } = await listProducts({
        queryParams: {
          id: Array.from(productIds),
          limit: productIds.size,
        },
        countryCode,
      })

      const priceMap = new Map<string, HttpTypes.StoreProduct>()
      response.products.forEach((product) => {
        if (product.id) {
          priceMap.set(product.id, product)
        }
      })

      setProductPrices(priceMap)
    } catch (error) {
      console.error("Error fetching product prices:", error)
    } finally {
      setIsLoadingPrices(false)
    }
  }

  fetchProductPrices()
}, [builder.complementary_products, builder.addons, countryCode])

// TODO add return statement
```

This `useEffect` hook is triggered whenever the country code, complementary products, or addons change, ensuring that the latest pricing information is fetched for the selected products.

You fetch the prices using the `listProducts` function, and you store the prices in a map. You'll use this map to display the prices of complementary and addon product variants.

Finally, you need to add a `return` statement to the `ProductBuilderConfig` component. Replace the `TODO` with the following:

```tsx title="src/modules/products/components/product-builder-config/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div className="flex flex-col gap-y-4">
    {/* Custom Fields Section */}
    {showCustomFields && (
      <>
        <div className="flex flex-col gap-y-4">
          {builder.custom_fields.map((field) => {
            const currentValue = customFields.find((f) => f.field_id === field.id)?.value || ""
            
            return (
              <div key={field.id} className="space-y-3">
                <div className="flex items-center gap-1">
                  <span className="text-sm">{field.name}</span>
                  {field.is_required && (
                    <span className="text-ui-tag-red-text text-sm">*</span>
                  )}
                </div>
                {field.description && (
                  <Text className="text-ui-fg-subtle text-xs">
                    {field.description}
                  </Text>
                )}
                
                <Input
                  type={field.type}
                  value={currentValue}
                  onChange={(e) => handleCustomFieldChange(
                    field.id, 
                    field.type === "number" ? parseFloat(e.target.value) || 0 : e.target.value
                  )}
                  placeholder={`Enter ${field.name.toLowerCase()}`}
                />
              </div>
            )
          })}
        </div>
        <Divider />
      </>
    )}

    {/* Complementary Products Section */}
    {showComplementaryProducts && (
      <>
        <div className="flex flex-col gap-y-4">
          {builder.complementary_products
            .map((compProduct) => {
            const product = compProduct.product
            const productWithPrices = productPrices.get(product.id!)
            
            return (
              <div key={compProduct.id} className="space-y-3">
                <span className="text-sm">Add a {product.title}</span>
                <Text className="text-ui-fg-subtle text-xs">
                  Complete your setup with perfectly matched accessories and essentials
                </Text>
                
                <div className="space-y-2">
                  {(productWithPrices?.variants || product.variants || []).map((variant) => {
                    const isSelected = complementaryProducts.some((p) => p.variant_id === variant.id)
                    
                    return (
                      <VariantSelector
                        key={variant.id}
                        variant={variant}
                        product={productWithPrices || product}
                        isSelected={isSelected}
                        isLoading={isLoadingPrices}
                        onToggle={handleComplementaryToggle}
                      />
                    )
                  })}
                </div>
              </div>
            )
          })}
        </div>
        <Divider />
      </>
    )}

    {/* Addons Section */}
    {showAddons && (
      <>
        <div className="space-y-3">
          <div className="flex items-center gap-1">
            <span className="text-sm">Protect & Enhance Your Purchase</span>
          </div>
          <Text className="text-ui-fg-subtle text-xs">
            Add peace of mind with premium features
          </Text>
          
          <div className="flex flex-col gap-y-3">
            {builder.addons
              .map((addon) => {
                const product = addon.product
                const productWithPrices = productPrices.get(product.id!)
                
                return (
                  <div key={addon.id} className="space-y-2">
                    {(productWithPrices?.variants || product.variants || []).map((variant) => {
                      const isSelected = addons.some((a) => a.variant_id === variant.id)
                      
                      return (
                        <VariantSelector
                          key={variant.id}
                          variant={variant}
                          product={productWithPrices || product}
                          isSelected={isSelected}
                          isLoading={isLoadingPrices}
                          onToggle={handleAddonToggle}
                        />
                      )
                    })}
                  </div>
                )
              })}
          </div>
        </div>
        {/* Only add separator if not the last section */}
        {isLoadingPrices && <Divider />}
      </>
    )}

    {/* Loading State */}
    {isLoadingPrices && (
      <div className="flex items-center justify-center py-4">
        <Text className="text-ui-fg-subtle text-sm">Loading prices...</Text>
      </div>
    )}

    {(showCustomFields || showComplementaryProducts || showAddons) && <Divider />}
  </div>
)
```

You display a separate section for each custom field, complementary product, and addon in the product builder configuration. You also use the `VariantSelector` component to display the variants of each complementary and addon product.

### e. Modify Price Component to Include Builder Prices

When a customer chooses complementary and addon products, the price shown on the product page should reflect that selection. So, you need to modify the pricing component to accept the builder configuration, and update the displayed price accordingly.

In `src/modules/products/components/product-price/index.tsx`, add the following imports at the top of the file:

```tsx title="src/modules/products/components/product-price/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { BuilderConfiguration } from "../../../../types/global"
import { convertToLocale } from "@lib/util/money"
```

Then, replace the `ProductPrice` component with the following:

```tsx title="src/modules/products/components/product-price/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={productPriceHighlights}
export default function ProductPrice({
  product,
  variant,
  builderConfig,
}: {
  product: HttpTypes.StoreProduct
  variant?: HttpTypes.StoreProductVariant
  builderConfig?: BuilderConfiguration | null
}) {
  const { cheapestPrice, variantPrice } = getProductPrice({
    product,
    variantId: variant?.id,
  })

  const selectedPrice = variant ? variantPrice : cheapestPrice

  // Calculate total price including builder configuration
  const calculateTotalPrice = () => {
    if (!selectedPrice) {return null}

    let totalPrice = selectedPrice.calculated_price_number || 0
    let totalOriginalPrice = selectedPrice.original_price_number || selectedPrice.calculated_price_number || 0

    if (builderConfig) {
      // Add complementary products prices
      builderConfig.complementary_products.forEach((comp) => {
        totalPrice += comp.price || 0
        totalOriginalPrice += comp.price || 0
      })

      // Add addons prices
      builderConfig.addons.forEach((addon) => {
        const addonPrice = (addon.price || 0) * (addon.quantity || 1)
        totalPrice += addonPrice
        totalOriginalPrice += addonPrice
      })
    }

    const currencyCode = selectedPrice.currency_code || "USD"

    return {
      calculated_price_number: totalPrice,
      original_price_number: totalOriginalPrice,
      calculated_price: convertToLocale({
        amount: totalPrice,
        currency_code: currencyCode,
      }),
      original_price: convertToLocale({
        amount: totalOriginalPrice,
        currency_code: currencyCode,
      }),
      price_type: selectedPrice.price_type,
      percentage_diff: selectedPrice.percentage_diff,
    }
  }

  const finalPrice = calculateTotalPrice()

  if (!finalPrice) {
    return <div className="block w-32 h-9 bg-gray-100 animate-pulse" />
  }

  return (
    <div className="flex flex-col text-ui-fg-base">
      <span
        className={clx("text-xl-semi", {
          "text-ui-fg-interactive": finalPrice.price_type === "sale",
        })}
      >
        {!variant && "From "}
        <span
          data-testid="product-price"
          data-value={finalPrice.calculated_price_number}
        >
          {finalPrice.calculated_price}
        </span>
      </span>
      {finalPrice.price_type === "sale" && (
        <>
          <p>
            <span className="text-ui-fg-subtle">Original: </span>
            <span
              className="line-through"
              data-testid="original-product-price"
              data-value={finalPrice.original_price_number}
            >
              {finalPrice.original_price}
            </span>
          </p>
          <span className="text-ui-fg-interactive">
            -{finalPrice.percentage_diff}%
          </span>
        </>
      )}
    </div>
  )
}
```

You make the following key changes:

- Add the `builderConfig` prop to the `ProductPrice` component.
- Add a `calculateTotalPrice` function to compute the total price including the builder configuration.
- Remove the existing condition on `selectedPrice`, and replace it instead with a condition on `finalPrice`. The condition's body is still the same.
- Modify the return statement to use `finalPrice` instead of `selectedPrice`.

### f. Display Product Builder on Product Page

Finally, to display the product builder component on the product details page, you need to modify two components:

- `ProductActions` that displays the product variant options with an add-to-cart button.
- `MobileActions` that displays the product variant options in a mobile-friendly format.

#### Customize ProductActions

You'll start with modifying the `ProductActions` component to include the product builder.

In `src/modules/products/components/product-actions/index.tsx`, add the following imports:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import ProductBuilderConfig from "../product-builder-config"
import { ProductWithBuilder, BuilderConfiguration } from "../../../../types/global"
import { hasProductBuilder } from "@lib/util/product-builder"
```

Next, change the type of the `product` prop to `ProductWithBuilder`:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["2"]]}
type ProductActionsProps = {
  product: ProductWithBuilder
  // ...
}
```

Then, in the `ProductActions` component, add the following state variables and `useEffect` hook:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={productActionHighlights}
export default function ProductActions({
  product,
  disabled,
}: ProductActionsProps) {
  // ...
  const [builderConfig, setBuilderConfig] = useState<BuilderConfiguration | null>(null)
  const [isBuilderConfigValid, setIsBuilderConfigValid] = useState(true)

  // Initialize validation state for products without builder
  useEffect(() => {
    if (!hasProductBuilder(product)) {
      setIsBuilderConfigValid(true)
    }
  }, [product])

  // ...
}
```

You define two variables:

- `builderConfig`: Holds the configuration for the product builder, if it exists.
- `isBuilderConfigValid`: Tracks the validity of the builder configuration.

You also add a `useEffect` hook to initialize the builder configuration state when the product changes.

Next, you'll make updates to the `return` statement. Find the `ProductPrice` usage in the `return` statement and replace it with the following:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <>
    {/* ... */}
    {hasProductBuilder(product) && (
      <>
        <ProductBuilderConfig
          product={product}
          countryCode={countryCode}
          onConfigurationChange={setBuilderConfig}
          onValidationChange={setIsBuilderConfigValid}
        />
      </>
    )}

    <ProductPrice 
      product={product} 
      variant={selectedVariant} 
      builderConfig={builderConfig}
    />
    {/* ... */}
  </>
)
```

You display the `ProductBuilderConfig` component before the price, and you pass the builder configurations to the `ProductPrice` component.

Finally, find the add-to-cart button and replace it with the following:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["12"], ["23"], ["24"]]}
return (
  <>
    {/* ... */}
    <Button
      onClick={handleAddToCart}
      disabled={
        !inStock ||
        !selectedVariant ||
        !!disabled ||
        isAdding ||
        !isValidVariant ||
        (hasProductBuilder(product) && !isBuilderConfigValid)
      }
      variant="primary"
      className="w-full h-10"
      isLoading={isAdding}
      data-testid="add-product-button"
    >
      {!selectedVariant && !options
        ? "Select variant"
        : !inStock || !isValidVariant
        ? "Out of stock"
        : hasProductBuilder(product) && !isBuilderConfigValid
        ? "Complete required fields"
        : "Add to cart"}
    </Button>
    {/* ... */}
  </>
)
```

You modify the button's `disabled` prop to also account for the validity of the product builder configuration, and you show the correct button text based on that validity.

#### Customize MobileActions

Next, you'll customize the `MobileActions` to show the correct price and button text in mobile view.

In `src/modules/products/components/product-actions/mobile-actions.tsx`, add the following imports at the top of the file:

```tsx title="src/modules/products/components/product-actions/mobile-actions.tsx" badgeLabel="Storefront" badgeColor="blue"
import { BuilderConfiguration } from "../../../../types/global"
import { convertToLocale } from "@lib/util/money"
import { hasProductBuilder } from "@lib/util/product-builder"
```

Next, add the following props to the `MobileActionsProps` type:

```tsx title="src/modules/products/components/product-actions/mobile-actions.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["3"], ["4"]]}
type MobileActionsProps = {
  // ...
  builderConfig?: BuilderConfiguration | null
  isBuilderConfigValid?: boolean
}
```

The component now accepts the builder configuration and its validity state as props.

Next, add the props to the destructured parameter of the `MobileActions` component:

```tsx title="src/modules/products/components/product-actions/mobile-actions.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["3"], ["4"]]}
const MobileActions: React.FC<MobileActionsProps> = ({
  // ...
  builderConfig,
  isBuilderConfigValid = true,
}: MobileActionsProps) => {
  // ...
}
```

After that, find the `selectedPrice` variable and replace it with the following:

```tsx title="src/modules/products/components/product-actions/mobile-actions.tsx" badgeLabel="Storefront" badgeColor="blue"
const selectedPrice = useMemo(() => {
  if (!price) {
    return null
  }
  const { variantPrice, cheapestPrice } = price
  const basePrice = variantPrice || cheapestPrice || null
  
  if (!basePrice) {return null}

  // Calculate total price including builder configuration
  let totalPrice = basePrice.calculated_price_number || 0

  if (builderConfig) {
    // Add complementary products prices
    builderConfig.complementary_products.forEach((comp) => {
      totalPrice += comp.price || 0
    })

    // Add addons prices
    builderConfig.addons.forEach((addon) => {
      const addonPrice = (addon.price || 0) * (addon.quantity || 1)
      totalPrice += addonPrice
    })
  }

  const currencyCode = basePrice.currency_code || "USD"

  return {
    ...basePrice,
    calculated_price_number: totalPrice,
    calculated_price: convertToLocale({
      amount: totalPrice,
      currency_code: currencyCode,
    }),
  }
}, [price, builderConfig])
```

Similar to the `ProductPrice` component, you set the selected price to the total price calculated from the builder configuration. This ensures that the correct price is displayed in the mobile view as well.

Finally, in the `return` statement, find the add-to-cart button and replace it with the following:

```tsx title="src/modules/products/components/product-actions/mobile-actions.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["9"], ["19"], ["20"]]}
return (
  <>
    {/* ... */}
    <Button
      onClick={handleAddToCart}
      disabled={
        !inStock || 
        !variant || 
        (hasProductBuilder(product) && !isBuilderConfigValid)
      }
      className="w-full"
      isLoading={isAdding}
      data-testid="mobile-cart-button"
    >
      {!variant
        ? "Select variant"
        : !inStock
        ? "Out of stock"
        : hasProductBuilder(product) && !isBuilderConfigValid
        ? "Complete required fields"
        : "Add to cart"}
    </Button>
    {/* ... */}
  </>
)
```

Similar to the `ProductActions` component, you ensure the button's disabled state and text match the builder configuration's validity.

### Test Product Details Page

To test out the product details page in the Next.js Starter Storefront:

1. Start the Medusa application with the following command:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

2. Start the Next.js Starter Storefront with the following command:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

3. In the storefront, go to Menu -> Store.
4. Click on the product that has builder configurations.

You should see the custom fields, complementary products, and addons on the product's page.

![Product details page with builder configurations](https://res.cloudinary.com/dza7lstvk/image/upload/v1755101580/Medusa%20Resources/CleanShot_2025-08-13_at_19.12.48_2x_tf4goa.png)

While you can enter custom values and select variants, you still can't add the product variant with its builder configurations to the cart. You'll support that in the next step.

***

## Step 8: Add Product with Builder Configurations to Cart

In this step, you'll create a workflow that adds products with their builder configurations to the cart, then expose that functionality in an API route that you can send requests to from the storefront.

### a. Create Workflow

The workflow will validate the builder configurations, add the main product variant to the cart, then add the complementary and addon products as separate line items. You'll also associate the items with one another using their metadata.

The workflow will have the following steps:

- [validateProductBuilderConfigurationStep](#validateProductBuilderConfigurationStep): Validates the product builder configuration
- [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md): Acquires a lock on the cart to prevent concurrent modifications.
- [addToCartWorkflow](#addToCartWorkflow): Adds the product to the cart.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Get cart with items details.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Get updated cart details.
- [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md): Releases the lock on the cart.

You only need to implement the `validateProductBuilderConfigurationStep`, as Medusa provides the rest.

#### validateProductBuilderConfigurationStep

The `validateProductBuilderConfigurationStep` ensures the chosen builder configurations are valid before proceeding with the cart addition.

To create the step, create the file `src/workflows/steps/validate-product-builder-configuration.ts` with the following content:

```ts title="src/workflows/steps/validate-product-builder-configuration.ts" highlights={validateProductBuilderConfigurationStepHighlights1}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { MedusaError } from "@medusajs/framework/utils"

export type ValidateProductBuilderConfigurationStepInput = {
  product_id: string
  custom_field_values?: Record<string, any>
  complementary_product_variants?: string[] 
  addon_variants?: string[]
}

export const validateProductBuilderConfigurationStep = createStep(
  "validate-product-builder-configuration",
  async ({
    product_id,
    custom_field_values,
    complementary_product_variants,
    addon_variants,
  }: ValidateProductBuilderConfigurationStepInput, { container }) => {
    const query = container.resolve("query")

    const { data: [productBuilder] } = await query.graph({
      entity: "product_builder",
      fields: [
        "*",
        "custom_fields.*",
        "complementary_products.*",
        "complementary_products.product.variants.*",
        "addons.*",
        "addons.product.variants.*",
      ],
      filters: {
        product_id,
      },
    })

    if (!productBuilder) {
      throw new MedusaError(
        MedusaError.Types.NOT_FOUND,
        `Product builder configuration not found for product ID: ${product_id}`
      )
    }

    // TODO validate custom fields, complementary products, and addon products
  }
)
```

This step receives the product ID and its builder configurations as input.

In the step, you resolve Query and retrieve the product's builder configurations. If the product builder is not found, you throw an error.

Next, you need to validate the custom fields to ensure they match the product builder custom fields. Replace the `TODO` with the following:

```ts title="src/workflows/steps/validate-product-builder-configuration.ts" highlights={validateProductBuilderConfigurationStepHighlights2}
if (
  !productBuilder.custom_fields.length && 
  custom_field_values && Object.keys(custom_field_values).length > 0
) {
  throw new MedusaError(
    MedusaError.Types.INVALID_DATA,
    `Product doesn't support custom fields.`
  )
}

for (const field of productBuilder.custom_fields) {
  if (!field) {
    continue
  }
  const value = custom_field_values?.[field.name]
  
  // Check required fields
  if (field.is_required && (!value || value === "")) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      `Custom field "${field.name}" is required`
    )
  }

  // Validate field type
  if (value !== undefined && value !== "" && field.type === "number" && isNaN(Number(value))) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      `Custom field "${field.name}" must be a number`
    )
  }
}

// TODO validate complementary products and addon products
```

You validate the selected custom fields to ensure that:

- The product supports custom fields.
- Each required custom field is provided and has a valid value.
- The values of custom fields match their defined types (e.g., numbers are actually numbers).

Next, you need to validate the complementary products and addon products. Replace the `TODO` with the following:

```ts title="src/workflows/steps/validate-product-builder-configuration.ts" highlights={validateProductBuilderConfigurationStepHighlights3}
const invalidComplementary = complementary_product_variants?.filter(
  (id) => !productBuilder.complementary_products.some((cp) => 
    cp?.product?.variants.some((variant) => variant.id === id)
  )
)

if ((invalidComplementary?.length || 0) > 0) {
  throw new MedusaError(
    MedusaError.Types.INVALID_DATA,
    `Invalid complementary product variants: ${invalidComplementary!.join(", ")}`
  )
}

const invalidAddons = addon_variants?.filter(
  (id) => !productBuilder.addons.some((addon) => 
    addon?.product?.variants.some((variant) => variant.id === id)
  )
)

if ((invalidAddons?.length || 0) > 0) {
  throw new MedusaError(
    MedusaError.Types.INVALID_DATA,
    `Invalid addon product variants: ${invalidAddons!.join(", ")}`
  )
}

return new StepResponse(productBuilder)
```

You apply the same validation to complementary and addon products. You make sure the selected product variants exist in the product builder's complementary and addon products.

Finally, if all configurations are valid, you return the product builder configurations.

#### Implement Workflow

You can now implement the workflow that adds products with builder configurations to the cart.

Create the file `src/workflows/add-product-builder-to-cart.ts` with the following content:

```ts title="src/workflows/add-product-builder-to-cart.ts" collapsibleLines="1-17" expandButtonLabel="Show Imports" highlights={addToCartWorkflowHighlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  transform,
  when,
} from "@medusajs/framework/workflows-sdk"
import { 
  addToCartWorkflow, 
  updateLineItemInCartWorkflow, 
  useQueryGraphStep, 
  acquireLockStep, 
  releaseLockStep,
} from "@medusajs/medusa/core-flows"
import { 
  validateProductBuilderConfigurationStep,
} from "./steps/validate-product-builder-configuration"

type AddProductBuilderToCartInput = {
  cart_id: string
  product_id: string
  variant_id: string
  quantity?: number
  custom_field_values?: Record<string, any>
  complementary_product_variants?: string[] // Array of product IDs
  addon_variants?: string[] // Array of addon product IDs
}

export const addProductBuilderToCartWorkflow = createWorkflow(
  "add-product-builder-to-cart",
  (input: AddProductBuilderToCartInput) => {
    // Step 1: Validate the product builder configuration and selections
    const productBuilder = validateProductBuilderConfigurationStep({
      product_id: input.product_id,
      custom_field_values: input.custom_field_values,
      complementary_product_variants: input.complementary_product_variants,
      addon_variants: input.addon_variants,
    })

    acquireLockStep({
      key: input.cart_id,
      timeout: 2,
      ttl: 10,
    })

    // TODO add main, complementary, and addon product variants to the cart
  }
)
```

The workflow accepts the cart, product, variant, and builder configuration information as input.

So far, you validate the product builder configuration using the step you created earlier. If the validation fails, the workflow will stop executing. You also acquire a lock on the cart to prevent concurrent modifications.

Next, you need to add the main product variant to the cart. Replace the `TODO` with the following:

```ts title="src/workflows/add-product-builder-to-cart.ts" highlights={addProductBuilderToCartWorkflowHighlights2}
// Step 2: Add main product to cart
const addMainProductData = transform({
  input,
  productBuilder,
}, (data) => ({
    cart_id: data.input.cart_id,
    items: [{
    variant_id: data.input.variant_id,
    quantity: data.input.quantity || 1,
    metadata: {
      product_builder_id: data.productBuilder?.id,
      custom_fields: Object.entries(data.input.custom_field_values || {})
        .map(([field_id, value]) => {
          const field = data.productBuilder?.custom_fields.find((f) => f?.id === field_id)
          return {
            field_id,
            name: field?.name,
            value,
          }
        }),
      is_builder_main_product: true,
    },
  }],
}))

addToCartWorkflow.runAsStep({
  input: addMainProductData,
})

// TODO add complementary and addon product variants to cart
```

You prepare the data to add the main product variant to the cart. You include in the item's metadata the product builder ID, any custom fields, and a flag to identify it as the main product in the builder configuration.

After that, you use the [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) to add the main product to the cart.

Next, you need to add the complementary and addon product variants to the cart. Replace the `TODO` with the following:

```ts title="src/workflows/add-product-builder-to-cart.ts" highlights={addProductBuilderToCartWorkflowHighlights3}
// Step 4: Get cart after main product is added
const { data: cartWithMainProduct } = useQueryGraphStep({
  entity: "cart",
  fields: ["*", "items.*"],
  filters: {
    id: input.cart_id,
  },
  options: {
    throwIfKeyNotFound: true,
  },
})

// Step 5: Add complementary and addon products
const {
  items_to_add: moreItemsToAdd,
  main_item_update: mainItemUpdate,
} = transform({
  input,
  cartWithMainProduct,
}, (data) => {
  if (!data.input.complementary_product_variants?.length && !data.input.addon_variants?.length) {
    return {}
  }

  // Find the main product line item (most recent addition with builder metadata)
  const mainLineItem = data.cartWithMainProduct[0].items.find((item: any) => 
    item.metadata?.is_builder_main_product === true
  )

  if (!mainLineItem) {
    return {}
  }
  
  return {
    items_to_add: {
      cart_id: data.input.cart_id,
      items: [
        ...(data.input.complementary_product_variants?.map((complementaryProductVariant) => ({
          variant_id: complementaryProductVariant,
          quantity: 1,
          metadata: {
            main_product_line_item_id: mainLineItem.id,
          },
        })) || []),
        ...(data.input.addon_variants?.map((addonVariant) => ({
          variant_id: addonVariant,
          quantity: 1,
          metadata: {
            main_product_line_item_id: mainLineItem.id,
            is_addon: true,
          },
        })) || []),
      ],
    },
    main_item_update: {
      item_id: mainLineItem.id,
      cart_id: data.cartWithMainProduct[0].id,
      update: {
        metadata: {
          cart_line_item_id: mainLineItem.id,
        },
      },
    },
  }
})

when({
  moreItemsToAdd,
  mainItemUpdate,
}, ({ 
  moreItemsToAdd,
  mainItemUpdate,
}) => !!moreItemsToAdd && moreItemsToAdd.items.length > 0 && !!mainItemUpdate)
.then(() => {
  addToCartWorkflow.runAsStep({
    input: {
      cart_id: moreItemsToAdd!.cart_id,
      items: moreItemsToAdd!.items,
    },
  })
  // @ts-ignore
  .config({ name: "add-more-products-to-cart" })

  updateLineItemInCartWorkflow.runAsStep({
    input: mainItemUpdate!,
  })
})

// TODO retrieve and return updated cart details
```

First, you retrieve the cart after adding the main product to get its line items.

Then, you prepare the data to add the complementary and addon products to the cart. You include the main product line item ID in their metadata to associate them with the main product. Also, you set the `is_addon` flag for addon products.

You also prepare the data to update the main product line item's metadata with its cart line item ID. This allows you to reference it after the order is placed, since the `metadata` is moved to the order line item's `metadata`.

Finally, you add the complementary and addon products to the cart using the `addToCartWorkflow`, and update the product's `metadata` with the cart line item ID.

The last thing you need to do is retrieve the updated cart details after adding all items and return them. Replace the `TODO` with the following:

```ts title="src/workflows/add-product-builder-to-cart.ts"
// Step 6: Fetch the final updated cart
const { data: updatedCart } = useQueryGraphStep({
  entity: "cart",
  fields: ["*", "items.*"],
  filters: {
    id: input.cart_id,
  },
  options: {
    throwIfKeyNotFound: true,
  },
}).config({ name: "get-final-cart" })

releaseLockStep({
  key: input.cart_id,
})

return new WorkflowResponse({
  cart: updatedCart[0],
})
```

You retrieve the final cart details after all items have been added. Then, you release the lock on the cart and return the updated cart in the workflow response.

### b. Create API Route

Next, you'll create the API route that executes the above workflow.

To create the API route, create the file `src/api/store/carts/[id]/product-builder/route.ts` with the following content:

```ts title="src/api/store/carts/[id]/product-builder/route.ts" highlights={addToCartApiHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { addProductBuilderToCartWorkflow } from "../../../../../workflows/add-product-builder-to-cart"
import { z } from "zod"

export const AddBuilderProductSchema = z.object({
  product_id: z.string(),
  variant_id: z.string(),
  quantity: z.number().optional().default(1),
  custom_field_values: z.record(z.any()).optional().default({}),
  complementary_product_variants: z.array(z.string()).optional().default([]),
  addon_variants: z.array(z.string()).optional().default([]),
})

export async function POST(
  req: MedusaRequest<
    z.infer<typeof AddBuilderProductSchema>
  >,
  res: MedusaResponse
) {

  const cartId = req.params.id

  const { result } = await addProductBuilderToCartWorkflow(req.scope).run({
    input: {
      cart_id: cartId,
      ...req.validatedBody,
    },
  })

  res.json({
    cart: result.cart,
  })
}
```

You define a Zod schema to validate the request body, then you expose a `POST` API route at `/store/carts/[id]/product-builder`.

In the route handler, you execute the `addProductBuilderToCartWorkflow` with the validated request body. You return the updated cart in the response.

### c. Add Validation Middleware

To validate the request body before it reaches the API route, you need to add a validation middleware.

In `src/api/middlewares.ts`, add the following import at the top of the file:

```ts title="src/api/middlewares.ts"
import { AddBuilderProductSchema } from "./store/carts/[id]/product-builder/route"
```

Then, add the following object to the `routes` array in `defineMiddlewares`:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/store/carts/:id/product-builder",
      methods: ["POST"],
      middlewares: [
        validateAndTransformBody(AddBuilderProductSchema),
      ],
    },
  ],
})
```

You apply the `validateAndTransformBody` middleware to the `/store/carts/:id/product-builder` route, passing it the Zod schema you created for validation.

You'll test out this API route and functionality when you customize the storefront next.

***

## Step 9: Customize Cart in Storefront

In this step, you'll customize the storefront to:

- Support adding products with builder configurations to the cart.
- Display addon products with their main product in the cart.
- Display custom field values in the cart.

### a. Use Add Product Builder to Cart API Route

You'll start by customizing existing add-to-cart functionality to use the new API route you created in the previous step.

#### Define Line Item Types

In `src/types/global.ts`, add the following type definitions useful for your cart customizations:

```ts title="src/types/global.ts" badgeLabel="Storefront" badgeColor="blue"
export type BuilderLineItemMetadata = {
  is_builder_main_product?: boolean
  main_product_line_item_id?: string
  product_builder_id?: string
  custom_fields?: {
    field_id: string
    name?: string
    value: string
  }[]
  is_addon?: boolean
  cart_line_item_id?: string
}

export type LineItemWithBuilderMetadata = StoreCartLineItem & {
  metadata?: BuilderLineItemMetadata
}
```

You define the `BuilderLineItemMetadata` type to include all relevant metadata for line items that are part of a product builder configuration, and the `LineItemWithBuilderMetadata` type extends the existing line item type to include this metadata.

#### Identify Product Builder Items Utility

Next, you need a utility function to identify whether a line item belongs to a product with builder configurations.

In `src/lib/util/product-builder.ts`, add the following import at the top of the file:

```ts title="src/lib/util/product-builder.ts" badgeLabel="Storefront" badgeColor="blue"
import { LineItemWithBuilderMetadata } from "../../types/global"
```

Then, add the following function at the end of the file:

```ts title="src/lib/util/product-builder.ts" badgeLabel="Storefront" badgeColor="blue"
export function isBuilderLineItem(lineItem: LineItemWithBuilderMetadata): boolean {
  return lineItem?.metadata?.is_builder_main_product === true
}
```

You'll use this function in the next customizations.

#### Add Builder Product to Cart Function

In this section, you'll add a server function that sends a request to the API route you created earlier. You'll use this function when adding products with builder configurations to the cart.

In `src/lib/data/cart.ts`, add the following imports at the top of the file:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
import { BuilderConfiguration, LineItemWithBuilderMetadata } from "../../types/global"
import { isBuilderLineItem } from "../util/product-builder"
```

Then, add the following function to the file:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
export async function addBuilderProductToCart({
  productId,
  variantId,
  quantity,
  countryCode,
  builderConfiguration,
}: {
  productId: string
  variantId: string
  quantity: number
  countryCode: string
  builderConfiguration?: BuilderConfiguration
}) {
  if (!variantId) {
    throw new Error("Missing variant ID when adding to cart")
  }

  const cart = await getOrSetCart(countryCode)

  if (!cart) {
    throw new Error("Error retrieving or creating cart")
  }

  // If no builder configuration, use regular addToCart
  if (!builderConfiguration) {
    return addToCart({ variantId, quantity, countryCode })
  }

  const headers = {
    ...(await getAuthHeaders()),
  }

  await sdk.client.fetch(`/store/carts/${cart.id}/product-builder`, {
    method: "POST",
    headers,
    body: {
      product_id: productId,
      variant_id: variantId,
      quantity,
      custom_field_values: builderConfiguration.custom_fields.reduce(
        (acc, field) => {
          acc[field.field_id] = field.value
          return acc
        },
        {} as Record<string, any>
      ),
      complementary_product_variants: builderConfiguration.complementary_products.map(
        (comp) => comp.variant_id
      ),
      addon_variants: builderConfiguration.addons.map((addon) => addon.variant_id),
    },
  })
  .then(async () => {
    const cartCacheTag = await getCacheTag("carts")
    revalidateTag(cartCacheTag)

    const fulfillmentCacheTag = await getCacheTag("fulfillment")
    revalidateTag(fulfillmentCacheTag)
  })
  .catch(medusaError)
}
```

This function adds a product with a builder configuration to the cart by sending a request to the API route you created earlier. If no builder configuration is provided, it falls back to the regular `addToCart` function (which is defined in the same file).

#### Use Add Builder Product to Cart Function

Finally, you'll use the `addBuilderProductToCart` function in the `ProductActions` component, where the add-to-cart button is located.

In `src/modules/products/components/product-actions/index.tsx`, add the following imports at the top of the file:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { 
  addBuilderProductToCart,
} from "@lib/data/cart"
import { 
  toast,
} from "@medusajs/ui"
```

Then, in the `ProductActions` component, find the `handleAddToCart` function and replace it with the following:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const handleAddToCart = async () => {
  if (!selectedVariant?.id) {return null}

  setIsAdding(true)

  try {
    // Check if product has builder configuration
    if (hasProductBuilder(product) && builderConfig) {
      await addBuilderProductToCart({
        productId: product.id!,
        variantId: selectedVariant.id,
        quantity: 1,
        countryCode,
        builderConfiguration: builderConfig,
      })
    } else {
      // Use regular addToCart for products without builder configuration
      await addToCart({
        variantId: selectedVariant.id,
        quantity: 1,
        countryCode,
      })
    }
  } catch (error) {
    toast.error(`Failed to add product to cart: ${error}`)
  } finally {
    setIsAdding(false)
  }
}
```

If the product has builder configurations, you call the `addBuilderProductToCart` function. Otherwise, you fall back to the regular `addToCart` function.

#### Test Add to Cart Functionality

To test out the add-to-cart functionality with builder configurations, make sure that both the Medusa application and the Next.js Starter Storefront are running.

Then, open the page of a product with builder configurations in the storefront. Select the configurations, and add them to the cart.

The cart will be updated with the main product and selected complementary and addon product variants.

![Cart dropdown showing the product with its complementary product](https://res.cloudinary.com/dza7lstvk/image/upload/v1755160166/Medusa%20Resources/CleanShot_2025-08-14_at_11.28.53_2x_bcskgr.png)

### b. Customize Cart Page

Next, you'll customize the cart page to display the custom field values of a product, and group addon products with the main product.

You'll start with some styling changes, then update the cart item rendering logic to include the custom fields and addon products.

![Screenshot showcasing which areas of the cart page will be updated and how they'll look like](https://res.cloudinary.com/dza7lstvk/image/upload/v1755160329/Medusa%20Resources/CleanShot_2025-08-14_at_11.31.50_2x_tpxlht.png)

#### Styling Changes

You'll first update the style of the quantity changer component for a better design.

![Screenshot showcasing the updated quantity changer component](https://res.cloudinary.com/dza7lstvk/image/upload/v1755160526/Medusa%20Resources/CleanShot_2025-08-14_at_11.35.07_2x_vtmnuz.png)

In `src/modules/cart/components/cart-item-select/index.tsx`, replace the file content with the following:

```tsx title="src/modules/cart/components/cart-item-select/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={cartItemSelectHighlights}
"use client"

import { IconButton, clx } from "@medusajs/ui"
import {
  SelectHTMLAttributes,
  forwardRef,
  useEffect,
  useImperativeHandle,
  useRef,
  useState,
} from "react"
import { Minus, Plus } from "@medusajs/icons"

type NativeSelectProps = {
  placeholder?: string
  errors?: Record<string, unknown>
  touched?: Record<string, unknown>
  max?: number
  onQuantityChange?: (quantity: number) => void
} & SelectHTMLAttributes<HTMLInputElement>

const CartItemSelect = forwardRef<HTMLInputElement, NativeSelectProps>(
  ({ className, children, value: initialValue, onQuantityChange, ...props }, ref) => {
    const innerRef = useRef<HTMLInputElement>(null)
    const [value, setValue] = useState<number>(initialValue as number || 1)

    useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(
      ref,
      () => innerRef.current
    )

    const onMinus = () => {
      setValue((prevValue) => {
        return prevValue > 1 ? prevValue - 1 : 1
      })
    }

    const onPlus = () => {
      setValue((prevValue) => {
        return Math.min(prevValue + 1, props.max || Infinity)
      })
    }

    const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
      setValue(Math.min(parseInt(event.target.value) || 1, props.max || Infinity))
      onQuantityChange?.(value)
    }

    useEffect(() => {
      onQuantityChange?.(value)
    }, [value])

    return (
      <div
        className={clx(
          "flex items-center rounded-md bg-ui-bg-field shadow-borders-base h-10 w-fit",
          className
        )}>
        <input 
          {...props}
          type="number" 
          ref={innerRef}
          value={value}
          onChange={handleChange}
          className="py-3 px-4 w-[54px] bg-transparent flex justify-center items-end [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none focus:outline-none"
        />
        <IconButton variant="transparent" onClick={onMinus} className="border-x border-ui-border-base h-full rounded-none">
          <Minus />
        </IconButton>
        <IconButton variant="transparent" onClick={onPlus} className="h-full rounded-none">
          <Plus />
        </IconButton>
      </div>
    )
  }
)

CartItemSelect.displayName = "CartItemSelect"

export default CartItemSelect
```

You make the following key changes:

- Pass the `max` and `onQuantityChange` props to the `CartItemSelect` component.
- Use a `value` state variable to manage the input value.
- Add `+` and `-` buttons to increase or decrease the quantity.
- Call the `onQuantityChange` prop whenever the quantity changes.
- Show an input field rather than a select field for quantity.

Next, you'll update the styling of the delete button that removes items from the cart.

![Screenshot showcasing the updated delete button](https://res.cloudinary.com/dza7lstvk/image/upload/v1755160603/Medusa%20Resources/CleanShot_2025-08-14_at_11.36.29_2x_myhwyf.png)

In `src/modules/common/components/delete-button/index.tsx`, add the following import at the top of the file:

```tsx title="src/modules/common/components/delete-button/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { IconButton } from "@medusajs/ui"
```

Then, in the `DeleteButton` component, replace the `button` element in the `return` statement with the following:

```tsx title="src/modules/common/components/delete-button/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["6"], ["7"], ["8"], ["9"]]}
return (
  <div className={
    // ...
  }>
    {/* ... */}
    <IconButton variant="primary" onClick={() => handleDelete(id)}>
      {isDeleting ? <Spinner className="animate-spin" /> : <Trash />}
      <span>{children}</span>
    </IconButton>
  </div>
)
```

Next, you'll update the `LineItemOptions` component to receive a `className` prop that allows customizing its styles.

In `src/modules/common/components/line-item-options/index.tsx`, add the following import at the top of the file:

```tsx title="src/modules/common/components/line-item-options/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { clx } from "@medusajs/ui"
```

Next, update the `LineItemOptionsProps` to accept a `className` prop:

```tsx title="src/modules/common/components/line-item-options/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["3"]]}
type LineItemOptionsProps = {
  // ...
  className?: string
}
```

Then, destructure the `className` prop and use it in the `return` statement of the `LineItemOptions` component:

```tsx title="src/modules/common/components/line-item-options/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["3"], ["11"]]}
const LineItemOptions = ({
  // ...
  className,
}: LineItemOptionsProps) => {
  return (
    <Text
      data-testid={dataTestid}
      data-value={dataValue}
      className={clx(
        "inline-block txt-medium text-ui-fg-subtle w-full overflow-hidden text-ellipsis",
        className
      )}
    >
      Variant: {variant?.title}
    </Text>
  )
}
```

Next, you'll make small adjustments to the text size of the item price components.

![Screenshot showcasing the updated item price components](https://res.cloudinary.com/dza7lstvk/image/upload/v1755160898/Medusa%20Resources/CleanShot_2025-08-14_at_11.41.23_2x_eitu29.png)

In `src/modules/common/components/line-item-price/index.tsx`, update the `className` prop of the `span` element containing the price:

```tsx title="src/modules/common/components/line-item-price/index.tsx" highlights={[["5"]]} badgeLabel="Storefront" badgeColor="blue"
return (
  <div>
    {/* ... */}
    <span
      className={clx("txt-sm", {
        "text-ui-fg-interactive": hasReducedPrice,
      })}
      data-testid="product-price"
    >
      {convertToLocale({
        amount: currentPrice,
        currency_code: currencyCode,
      })}
    </span>
    {/* ... */}
  </div>
)
```

You change the `text-base-regular` class to `txt-small`.

Next, in `src/modules/common/components/line-item-unit-price/index.tsx`, update the `className` prop of the wrapper `div` and the `span` element containing the price:

```tsx title="src/modules/common/components/line-item-unit-price/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["2"], ["5"]]}
return (
  <div className="flex flex-col text-ui-fg-subtle justify-center h-full">
    {/* ... */}
    <span
      className={clx("txt-small", {
        "text-ui-fg-interactive": hasReducedPrice,
      })}
      data-testid="product-unit-price"
    >
      {convertToLocale({
        amount: total / item.quantity,
        currency_code: currencyCode,
      })}
    </span>
    {/* ... */}
  </div>
)
```

You changed the `text-ui-fg-muted` class in the wrapper `div` to `text-ui-fg-subtle`, and the `text-base-regular` class in the `span` element to `txt-small`.

#### Update Item Component

Next, you'll update the component showing a line item row. This component is used in multiple places, including the cart and checkout pages.

You'll update the component to ignore addon products. Instead, you'll show them as part of the main product line item. You'll also display the custom field values of the main product.

In `src/modules/cart/components/item/index.tsx`, add the following import at the top of the file:

```tsx title="src/modules/cart/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { LineItemWithBuilderMetadata } from "../../../../types/global"
import { isBuilderLineItem } from "../../../../lib/util/product-builder"
```

Then, add a `cartItems` prop to the `ItemProps`:

```tsx title="src/modules/cart/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["3"]]}
type ItemProps = {
  // ...
  cartItems?: HttpTypes.StoreCartLineItem[]
}
```

And add the prop to the `Item` component's destructured props:

```tsx title="src/modules/cart/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["1", "cartItems"]]}
const Item = ({ item, type = "full", currencyCode, cartItems }: ItemProps) => {
  // ...
}
```

Next, add the following in the component before the `return` statement:

```tsx title="src/modules/cart/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const Item = ({ item, type = "full", currencyCode, cartItems }: ItemProps) => {
  // ...
  // Check if this is a main product builder item
  const itemWithMetadata = item as LineItemWithBuilderMetadata
  const isMainBuilderProduct = isBuilderLineItem(itemWithMetadata)

  // Find addon items for this main product
  const addonItems = isMainBuilderProduct && cartItems
    ? cartItems.filter((cartItem: any) => 
        cartItem.metadata?.main_product_line_item_id === item.id && 
        cartItem.metadata?.is_addon === true
      )
    : []

  // Don't render addon items as separate rows (they'll be shown under the main item)
  if (itemWithMetadata.metadata?.is_addon === true) {
    return null
  }

  // ...
}
```

You create an `itemWithMetadata` variable, which is a typed version of the `item` prop that includes the metadata fields you defined earlier.

Next, if the item being viewed is a main product with builder configurations, you retrieve its addon items. Otherwise, if it's an addon item, you return `null` to skip rendering it as a separate row.

Finally, update the `return` statement to the following:

```tsx title="src/modules/cart/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={itemComponentHighlights}
return (
  <>
    <Table.Row className={clx(
      "w-full",
      addonItems.length > 0 ? "border-b-0": ""
    )} data-testid="product-row">
      <Table.Cell className="!pl-0 py-6 w-24">
        <LocalizedClientLink
          href={`/products/${item.product_handle}`}
          className={clx("flex", {
            "w-16": type === "preview",
            "small:w-24 w-12": type === "full",
          })}
        >
          <Thumbnail
            thumbnail={item.thumbnail}
            images={item.variant?.product?.images}
            size="square"
          />
        </LocalizedClientLink>
      </Table.Cell>

    <Table.Cell className="text-left py-7 w-2/3">
      <Text
        className="!txt-small-plus text-ui-fg-base"
        data-testid="product-title"
      >
        {item.product_title}
      </Text>
      <LineItemOptions variant={item.variant} data-testid="product-variant" className="txt-small" />
      {!!itemWithMetadata.metadata?.custom_fields && (
        <div className="inline-block overflow-hidden">
          {itemWithMetadata.metadata.custom_fields.map((field) => (
            <Text key={field.field_id} className="text-ui-fg-subtle txt-small">
              {field.name}: {field.value}
            </Text>
          ))}
        </div>
      )}
    </Table.Cell>

    {type === "full" && (
      <Table.Cell className="px-6">
        <div className="flex gap-2 items-center justify-end">
          <CartItemSelect
            value={item.quantity}
            onQuantityChange={(value) => {
              if (value === item.quantity) {
                return
              }
              changeQuantity(value)
            }}
            data-testid="product-select-button"
            max={maxQuantity}
          />
          <DeleteButton id={item.id} data-testid="product-delete-button" />
          {updating && <Spinner />}
        </div>
        <ErrorMessage error={error} data-testid="product-error-message" />
      </Table.Cell>
    )}

    {type === "full" && (
      <Table.Cell className="hidden small:table-cell px-6">
        <LineItemUnitPrice
          item={item}
          style="tight"
          currencyCode={currencyCode}
        />
      </Table.Cell>
    )}

    <Table.Cell className="px-6">
      <span
        className={clx("!pr-0", {
          "flex flex-col items-end h-full justify-center": type === "preview",
        })}
      >
        {type === "preview" && (
          <span className="flex gap-x-1 ">
            <Text className="text-ui-fg-muted">{item.quantity}x </Text>
            <LineItemUnitPrice
              item={item}
              style="tight"
              currencyCode={currencyCode}
            />
          </span>
        )}
        <LineItemPrice
          item={item}
          style="tight"
          currencyCode={currencyCode}
        />
      </span>
    </Table.Cell>
  </Table.Row>

  {/* Display addon items if this is a main builder product */}
  {isMainBuilderProduct && addonItems.length > 0 && addonItems.map((addon: any) => (
    <Table.Row key={addon.id} className="w-full" data-testid="addon-row">
      <Table.Cell className="!pl-0 py-6 w-24">
      </Table.Cell>
      <Table.Cell className="text-left !pl-0 py-7">
        <div className="flex items-center gap-2 py-1">
          <div className="flex flex-col gap-1">
            <Text
              data-testid="addon-title"
              className="!txt-small-plus text-ui-fg-base"
            >
              {addon.product_title}
            </Text>
            <div>
              <LineItemOptions variant={addon.variant} data-testid="addon-variant" className="txt-small" />
            </div>
          </div>
        </div>
      </Table.Cell>

      {type === "full" && (
        <Table.Cell className="px-6">
          <DeleteButton id={addon.id} data-testid="addon-delete-button" className="justify-end" />
        </Table.Cell>
      )}

      {type === "full" && (
        <Table.Cell className="hidden small:table-cell px-6">
          <LineItemUnitPrice
            item={addon}
            style="tight"
            currencyCode={currencyCode}
          />
        </Table.Cell>
      )}

      <Table.Cell className="px-6">
        <span
          className={clx("!pr-0", {
            "flex flex-col items-end h-full justify-center": type === "preview",
          })}
        >
          <LineItemPrice
            item={addon}
            style="tight"
            currencyCode={currencyCode}
          />
        </span>
      </Table.Cell>
    </Table.Row>
  ))}
  </>
)
```

You make the following key changes:

- Render the custom field values.
- Pass the new props to the `CartItemSelect` component.
- Render the add-on items after the main product item.
- Make general styling updates to improve the layout.

You also need to update the components that use the `Item` component to pass the new `cartItems` prop.

In `src/modules/cart/templates/items.tsx`, replace the `return` statement with the following:

```tsx title="src/modules/cart/templates/items.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={itemsTemplateHighlights}
const ItemsTemplate = ({ cart }: ItemsTemplateProps) => {
  // ...
  return (
    <div>
      <div className="pb-3 flex items-center">
        <Heading className="text-[2rem] leading-[2.75rem]">Cart</Heading>
      </div>
      <Table>
        <Table.Header className="border-t-0">
          <Table.Row className="text-ui-fg-subtle txt-medium-plus">
            <Table.HeaderCell className="!pl-0">Item</Table.HeaderCell>
            <Table.HeaderCell></Table.HeaderCell>
            <Table.HeaderCell className="px-6">Quantity</Table.HeaderCell>
            <Table.HeaderCell className="hidden small:table-cell px-6">
              Price
            </Table.HeaderCell>
            <Table.HeaderCell className="px-6">
              Total
            </Table.HeaderCell>
          </Table.Row>
        </Table.Header>
        <Table.Body>
          {items
            ? items
                .sort((a, b) => {
                  return (a.created_at ?? "") > (b.created_at ?? "") ? -1 : 1
                })
                .map((item) => {
                  return (
                    <Item
                      key={item.id}
                      item={item}
                      currencyCode={cart?.currency_code}
                      cartItems={items}
                    />
                  )
                })
            : repeat(5).map((i) => {
                return <SkeletonLineItem key={i} />
              })}
        </Table.Body>
      </Table>
    </div>
  )
}
```

You pass the `cartItems` prop to the `Item` component, and you pass new class names to other components for better styling.

Finally, in `src/modules/cart/templates/preview.tsx`, find the `Item` component in the `return` statement and update it to pass the `cartItems` prop:

```tsx title="src/modules/cart/templates/preview.tsx"  badgeLabel="Storefront" badgeColor="blue" highlights={[["13", "cartItems", "Pass new prop."]]}
const ItemsPreviewTemplate = ({ cart }: ItemsTemplateProps) => {
  // ...
  return (
    <div className={
      // ...
    }>
      {/* ... */}
      <Item
        key={item.id}
        item={item}
        type="preview"
        currencyCode={cart.currency_code}
        cartItems={cart.items}
      />
      {/* ... */}
    </div>
  )
}
```

#### Test out Changes

To test out the design changes to the cart page, make sure both the Medusa application and the Next.js Starter Storefront are running.

Then, open the cart page in the storefront. If you have a product with an addon and custom fields in the cart, you'll see them displayed within the main product's row.

![Screenshot showcasing the updated cart page with custom fields and addon products](https://res.cloudinary.com/dza7lstvk/image/upload/v1755161965/Medusa%20Resources/CleanShot_2025-08-14_at_11.59.16_2x_pfo20r.png)

### c. Update Cart Items Count in Dropdown

The cart dropdown at the top right of the page will display the total number of items in the cart, including addon products.

You'll update the cart dropdown to ignore the quantity of addon products when displaying the total count.

In `src/modules/layout/components/cart-dropdown/index.tsx`, add the following variable before the `totalItems` variable in the `CartDropdown` component:

```tsx title="src/modules/layout/components/cart-dropdown/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["7"]]}
const CartDropdown = ({
  cart: cartState,
}: {
  cart?: HttpTypes.StoreCart | null
}) => {
  // ...
  const filteredItems = cartState?.items?.filter((item) => !item.metadata?.is_addon)
  // ...
}
```

You filter the items to exclude any that are marked as add-ons.

Next, replace the `totalItems` declaration with the following:

```tsx title="src/modules/layout/components/cart-dropdown/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const CartDropdown = ({
  cart: cartState,
}: {
  cart?: HttpTypes.StoreCart | null
}) => {
  // ...
  const totalItems =
    filteredItems?.reduce((acc, item) => {
      return acc + item.quantity
    }, 0) || 0
  // ...
}
```

You calculate the total items by summing the quantities of the `filteredItems`, which excludes addon products.

Finally, in the `return` statement, replace all usages of `cartState.items` with `filteredItems`, and remove the children element of the `DeleteButton` for better styling:

```tsx title="src/modules/layout/components/cart-dropdown/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["6"], ["9"], ["21"], ["22"], ["23"], ["24"], ["25"]]}
return (
  <div
    // ...
  >
    {/* ... */}
    {cartState && filteredItems?.length ? (
      <>
        <div className="overflow-y-scroll max-h-[402px] px-4 grid grid-cols-1 gap-y-8 no-scrollbar p-px">
          {filteredItems
            .sort((a, b) => {
              return (a.created_at ?? "") > (b.created_at ?? "")
                ? -1
                : 1
            })
            .map((item) => (
              // ..
              <div
                // ...
              >
                {/* ... */}
                <DeleteButton
                  id={item.id}
                  className="mt-1"
                  data-testid="cart-item-remove-button"
                />
                {/* ... */}
              </div>
            ))
          }
        </div>
        {/* ... */}
      </>
    ) : (
      {/* ... */}
    )}
    {/* ... */}
  </div>
)
```

#### Test Cart Dropdown Changes

To test out the cart dropdown changes, make sure both the Medusa application and the Next.js Starter Storefront are running.

Then, check the "Cart" navigation item at the top right. The total count next to "Cart" should not include the addon products, and the dropdown should exclude them as well.

![Updated cart dropdown with updated total count and filtered items](https://res.cloudinary.com/dza7lstvk/image/upload/v1755160166/Medusa%20Resources/CleanShot_2025-08-14_at_11.28.53_2x_bcskgr.png)

***

## Step 10: Delete Product with Builder Configurations from Cart

In this step, you'll implement the logic to delete a product with builder configurations from the cart. This will include removing its addon products from the cart.

You'll create a workflow, use that workflow in an API route, then customize the storefront to use this API route when deleting a product with builder configurations from the cart.

### a. Remove Product with Builder Configurations from Cart Workflow

The workflow to remove a product with builder configurations from the cart has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve cart details.
- [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md): Acquire a lock on the cart to prevent concurrent modifications.
- [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/index.html.md): Delete line items from the cart.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the updated cart details.
- [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md): Release the lock on the cart.

Medusa provides all of these steps, so you can create the workflow without needing to implement any custom steps.

Create the file `src/workflows/remove-product-builder-from-cart.ts` with the following content:

```ts title="src/workflows/remove-product-builder-from-cart.ts" highlights={removeProductBuilderFromCartWorkflowHighlights} collapsibleLines="1-12" expandButtonLabel="Show Imports"
import { 
  createWorkflow, 
  WorkflowResponse,
  transform,
} from "@medusajs/framework/workflows-sdk"
import { 
  deleteLineItemsWorkflow, 
  useQueryGraphStep, 
  acquireLockStep, 
  releaseLockStep,
} from "@medusajs/medusa/core-flows"

type RemoveProductBuilderFromCartInput = {
  cart_id: string
  line_item_id: string
}

export const removeProductBuilderFromCartWorkflow = createWorkflow(
  "remove-product-builder-from-cart",
  (input: RemoveProductBuilderFromCartInput) => {
    // Step 1: Get current cart with all items
    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      fields: ["*", "items.*"],
      filters: {
        id: input.cart_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    acquireLockStep({
      key: input.cart_id,
      timeout: 2,
      ttl: 10,
    })

    // Step 2: Remove line item and its addons
    const itemsToRemove = transform({
      input,
      currentCart: carts,
    }, (data) => {
      const cart = data.currentCart[0]
      const targetLineItem = cart.items.find((item: any) => item.id === data.input.line_item_id)
      const lineItemIdsToRemove = [data.input.line_item_id]
      const isBuilderItem = targetLineItem?.metadata?.is_builder_main_product === true
      
      if (targetLineItem && isBuilderItem) {
        // Find all related addon items
        const relatedItems = cart.items.filter((item: any) => 
          item.metadata?.main_product_line_item_id === data.input.line_item_id &&
          item.metadata?.is_addon === true
        )
        
        // Add their IDs to the removal list
        lineItemIdsToRemove.push(
          ...relatedItems.map((item: any) => item.id)
        )
      }

      return {
        cart_id: data.input.cart_id,
        ids: lineItemIdsToRemove,
      }
    })

    deleteLineItemsWorkflow.runAsStep({
      input: itemsToRemove,
    })

    // Step 3: Get the updated cart
    const { data: updatedCart } = useQueryGraphStep({
      entity: "cart",
      fields: ["*", "items.*", "items.metadata"],
      filters: {
        id: input.cart_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    }).config({ name: "get-updated-cart" })

    releaseLockStep({
      key: input.cart_id,
    })

    return new WorkflowResponse({
      cart: updatedCart[0],
    })
  }
)
```

This workflow receives the IDs of the cart and the line item to remove.

In the workflow, you:

- Acquire a lock on the cart to prevent concurrent modifications.
- Retrieve the cart details with its items.
- Prepare the line items to remove by identifying the main product and its related addons.
- Remove the line items from the cart.
- Retrieve the updated cart details.
- Release the lock on the cart.

You return the cart details in the response.

### b. Remove Product with Builder Configurations from Cart API Route

Next, you'll create an API route that uses the workflow you created to remove a product with builder configurations from the cart.

Create the file `src/api/store/carts/[id]/product-builder/[item_id]/route.ts` with the following content:

```ts title="src/api/store/carts/[id]/product-builder/[item_id]/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { removeProductBuilderFromCartWorkflow } from "../../../../../../workflows/remove-product-builder-from-cart"

export const DELETE = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const {
    id: cartId,
    item_id: lineItemId,
  } = req.params

  const { result } = await removeProductBuilderFromCartWorkflow(req.scope)
    .run({
      input: {
        cart_id: cartId,
        line_item_id: lineItemId,
      },
    })

  res.json({
    cart: result.cart,
  })
}
```

You expose a `DELETE` API route at `/store/carts/[id]/product-builder/[item_id]`.

In the route handler, you execute the `removeProductBuilderFromCartWorkflow` with the cart and line item IDs from the request path parameters.

You return the cart details in the response.

### c. Use API Route in Storefront

Next, you'll customize the storefront to use the API route you created when deleting a product with builder configurations from the cart.

In `src/lib/data/cart.ts`, add the following function:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
export async function removeBuilderLineItem(lineItemId: string) {
  if (!lineItemId) {
    throw new Error("Missing lineItem ID when deleting builder line item")
  }

  const cartId = await getCartId()

  if (!cartId) {
    throw new Error("Missing cart ID when deleting builder line item")
  }

  const headers = {
    ...(await getAuthHeaders()),
  }

  await sdk.client
    .fetch(`/store/carts/${cartId}/product-builder/${lineItemId}`, {
      method: "DELETE",
      headers,
    })
    .then(async () => {
      const cartCacheTag = await getCacheTag("carts")
      revalidateTag(cartCacheTag)

      const fulfillmentCacheTag = await getCacheTag("fulfillment")
      revalidateTag(fulfillmentCacheTag)
    })
    .catch(medusaError)
}
```

This function sends a `DELETE` request to the API route you created earlier to remove a line item with builder configurations from the cart.

Next, to use this function when deleting a line item from the cart, you'll update the `DeleteButton` component to accept a new prop that determines whether the item belongs to a product with builder configurations.

In `src/modules/common/components/delete-button/index.tsx`, add the following import at the top of the file:

```tsx title="src/modules/common/components/delete-button/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { removeBuilderLineItem } from "@lib/data/cart"
```

Then, pass an `isBuilderConfigItem` prop to the `DeleteButton` component, and update its `handleDelete` function to use it:

```tsx title="src/modules/common/components/delete-button/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={deleteButtonHighlights}
const DeleteButton = ({
  // ...
  isBuilderConfigItem = false,
}: {
  // ...
  isBuilderConfigItem?: boolean
}) => {
  // ...
  const handleDelete = async (id: string) => {
    setIsDeleting(true)
    if (isBuilderConfigItem) {
      await removeBuilderLineItem(id).catch((err) => {
        setIsDeleting(false)
      })
    } else {
      await deleteLineItem(id).catch((err) => {
        setIsDeleting(false)
      })
    }
  }

  // ...
}
```

You update the `handleDelete` function to use the `removeBuilderLineItem` function if the `isBuilderConfigItem` prop is `true`. Otherwise, it uses the regular `deleteLineItem` function.

Next, you need to pass the `isBuilderConfigItem` prop to the `DeleteButton` component in the components using it.

In `src/modules/cart/components/item/index.tsx`, update the first `DeleteButton` component usage in the return statement to pass the `isBuilderConfigItem` prop:

```tsx title="src/modules/cart/components/item/index.tsx"  badgeLabel="Storefront" badgeColor="blue" highlights={[["7", "isBuilderConfigItem", "Pass new prop"]]}
return (
  <>
    {/* ... */}
    <DeleteButton 
      id={item.id} 
      data-testid="product-delete-button"
      isBuilderConfigItem={isMainBuilderProduct}
    />
    {/* ... */}
  </>
)
```

Don't update the `DeleteButton` for addon products, as they don't have builder configurations.

Then, in `src/modules/layout/components/cart-dropdown/index.tsx`, add the following import at the top of the file:

```tsx title="src/modules/layout/components/cart-dropdown/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { LineItemWithBuilderMetadata } from "../../../../types/global"
import { isBuilderLineItem } from "../../../../lib/util/product-builder"
```

Next, find the `DeleteButton` usage in the `return` statement and update it to pass the `isBuilderConfigItem` prop:

```tsx title="src/modules/layout/components/cart-dropdown/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["10", "isBuilderConfigItem", "Pass new prop"]]}
return (
  <div
    // ...
  >
    {/* ... */}
    <DeleteButton
      id={item.id}
      className="mt-1"
      data-testid="cart-item-remove-button"
      isBuilderConfigItem={
        isBuilderLineItem(item as LineItemWithBuilderMetadata)
      }
    />
    {/* ... */}
  </div>
)
```

### Test Deleting Product with Builder Configurations from Cart

To test out the changes, make sure both the Medusa application and the Next.js Starter Storefront are running.

Then, in the storefront, delete the product with builder configurations from the cart either from the cart page or the cart dropdown. The addon item will also be removed from the cart.

***

## Step 11: Show Product Builder Configurations in Order Confirmation

In this step, you'll customize the order confirmation page in the storefront to group addon products with their main product, similar to the cart page.

In `src/modules/order/components/item/index.tsx`, add the following import at the top of the file:

```tsx title="src/modules/order/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { clx } from "@medusajs/ui"
import { LineItemWithBuilderMetadata } from "../../../../types/global"
import { isBuilderLineItem } from "../../../../lib/util/product-builder"
```

Next, pass a new `orderItems` prop to the `Item` component and its prop type:

```tsx title="src/modules/order/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={orderItemHighlights}
type ItemProps = {
  // ...
  orderItems?: (HttpTypes.StoreCartLineItem | HttpTypes.StoreOrderLineItem)[]
}

const Item = ({ item, currencyCode, orderItems }: ItemProps) => {
  // ...
}
```

This prop will include all the items in the order, allowing you to find the addons of a main product.

After that, add the following in the `Item` component before the `return` statement:

```tsx title="src/modules/order/components/item/index.tsx" highlights={orderItemLogicHighlights} badgeLabel="Storefront" badgeColor="blue"
const Item = ({ item, currencyCode, orderItems }: ItemProps) => {
  // Check if this is a main product builder item
  const itemWithMetadata = item as LineItemWithBuilderMetadata
  const isMainBuilderProduct = isBuilderLineItem(itemWithMetadata)

  // Find addon items for this main product
  const addonItems = isMainBuilderProduct && orderItems
    ? orderItems.filter((orderItem: any) => 
        orderItem.metadata?.main_product_line_item_id === item.metadata?.cart_line_item_id && 
        orderItem.metadata?.is_addon === true
      )
    : []

  // Don't render addon items as separate rows (they'll be shown under the main item)
  if (itemWithMetadata.metadata?.is_addon === true) {
    return null
  }

  // ...
}
```

If the item is a main product, you retrieve its addons. If an item is an addon, you return `null` to skip rendering it as a separate row.

Finally, replace the `return` statement with the following:

```tsx title="src/modules/order/components/item/index.tsx" highlights={orderItemReturnsHighlights} badgeLabel="Storefront" badgeColor="blue"
return (
  <>
    <Table.Row className={clx(
      "w-full",
      addonItems.length > 0 ? "border-b-0": ""
    )} data-testid="product-row">
      <Table.Cell className="!pl-0 p-4 w-24">
        <div className="flex w-16">
          <Thumbnail thumbnail={item.thumbnail} size="square" />
        </div>
      </Table.Cell>

      <Table.Cell className="text-left">
        <Text
          className="txt-medium-plus text-ui-fg-base"
          data-testid="product-name"
        >
          {item.product_title}
        </Text>
        <LineItemOptions variant={item.variant} data-testid="product-variant" />
        {!!itemWithMetadata.metadata?.custom_fields && (
          <div className="inline-block overflow-hidden">
            {itemWithMetadata.metadata.custom_fields.map((field) => (
              <Text key={field.field_id} className="text-ui-fg-subtle txt-small">
                {field.name}: {field.value}
              </Text>
            ))}
          </div>
        )}
      </Table.Cell>

      <Table.Cell className="!pr-0">
        <span className="!pr-0 flex flex-col items-end h-full justify-center">
          <span className="flex gap-x-1 ">
            <Text className="text-ui-fg-muted">
              <span data-testid="product-quantity">{item.quantity}</span>x{" "}
            </Text>
            <LineItemUnitPrice
              item={item}
              style="tight"
              currencyCode={currencyCode}
            />
          </span>

          <LineItemPrice
            item={item}
            style="tight"
            currencyCode={currencyCode}
          />
        </span>
      </Table.Cell>
    </Table.Row>

    {/* Display addon items if this is a main builder product */}
    {isMainBuilderProduct && addonItems.length > 0 && addonItems.map((addon: any) => (
      <Table.Row key={addon.id} className="w-full" data-testid="addon-row">
        <Table.Cell className="!pl-0 p-4 w-24">
        </Table.Cell>
        <Table.Cell className="text-left !pl-0">
          <div className="flex items-center gap-2 py-1">
            <div className="flex flex-col gap-1">
              <Text
                data-testid="addon-title"
                className="txt-medium text-ui-fg-base"
              >
                {addon.product_title}
              </Text>
              <div>
                <LineItemOptions variant={addon.variant} data-testid="addon-variant" className="txt-small" />
              </div>
            </div>
          </div>
        </Table.Cell>

        <Table.Cell className="!pr-0">
          <span className="!pr-0 flex flex-col items-end h-full justify-center">
            <span className="flex gap-x-1 ">
              <Text className="text-ui-fg-muted">
                <span data-testid="addon-quantity">{addon.quantity}</span>x{" "}
              </Text>
              <LineItemUnitPrice
                item={addon}
                style="tight"
                currencyCode={currencyCode}
              />
            </span>

            <LineItemPrice
              item={addon}
              style="tight"
              currencyCode={currencyCode}
            />
          </span>
        </Table.Cell>
      </Table.Row>
    ))}
  </>
)
```

You make the following key changes:

- Show the custom field values of a product with builder configurations.
- Show addons as a row after the main product row.
- Other design and styling changes.

You need to pass the `orderItems` prop to the `Item` component in the components using it.

In `src/modules/order/components/items/index.tsx`, find the `Item` component in the return statement and add the `orderItems` prop:

```tsx title="src/modules/order/components/items/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["10", "orderItems", "Pass new prop"]]}
const Items = ({ order }: ItemsProps) => {
  // ...
  return (
    <div className="flex flex-col">
      {/* ... */}
      <Item
        key={item.id}
        item={item}
        currencyCode={order.currency_code}
        orderItems={items}
      />
      {/* ... */}
    </div>
  )
}
```

### Test Order Confirmation Page

To test out the changes in the order confirmation page, make sure both the Medusa application and the Next.js Starter Storefront are running.

Then, place an order with a product that has builder configurations. In the confirmation page, you'll see the product with its custom fields and addon items displayed similar to the cart page.

![Updated order confirmation page](https://res.cloudinary.com/dza7lstvk/image/upload/v1755168706/Medusa%20Resources/CleanShot_2025-08-14_at_13.51.33_2x_bdadlh.png)

***

## Step 12: Show Product Builder Configuration in Order Admin Page

In the last step, you'll inject an admin widget to the order details page that shows the product builder configurations for each item in the order.

To create the widget, create the file `src/admin/widgets/order-builder-details-widget.tsx` with the following content:

```tsx title="src/admin/widgets/order-builder-details-widget.tsx" highlights={orderWidgetHighlights}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading, Text, clx } from "@medusajs/ui"
import { DetailWidgetProps, AdminOrder } from "@medusajs/framework/types"

type BuilderLineItemMetadata = {
  is_builder_main_product?: boolean
  main_product_line_item_id?: string
  product_builder_id?: string
  custom_fields?: {
    field_id: string
    name?: string
    value: string
  }[]
  is_addon?: boolean
  cart_line_item_id?: string
}

type LineItemWithBuilderMetadata = {
  id: string
  product_title: string
  variant_title?: string
  quantity: number
  metadata?: BuilderLineItemMetadata
}

const OrderBuilderDetailsWidget = ({ 
  data: order,
}: DetailWidgetProps<AdminOrder>) => {
  const orderItems = (order.items || []) as LineItemWithBuilderMetadata[]

  // Find all builder main products (items with custom configurations)
  const builderItems = orderItems.filter((item) => 
    item.metadata?.is_builder_main_product || 
    item.metadata?.custom_fields?.length
  )

  // If no builder items, don't show the widget
  if (builderItems.length === 0) {
    return null
  }

  const getAddonItems = (mainItemId: string) => {
    return orderItems.filter((item) => 
      item.metadata?.main_product_line_item_id === mainItemId &&
      item.metadata?.is_addon === true
    )
  }

  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">Items with Builder Configurations</Heading>
      </div>

      <div className="px-6 py-4">
        {builderItems.map((item, index) => {
          const addonItems = getAddonItems(item.metadata?.cart_line_item_id || "")
          const isLastItem = index === builderItems.length - 1
          
          return (
            <div key={item.id} className={clx(
              "mb-6 last:mb-0",
              !isLastItem && "pb-6 border-b border-ui-border-base"
            )}>
              {/* Main Product Info */}
              <div className="flex items-start justify-between mb-3">
                <div className="flex-1">
                  <Text className="font-medium text-ui-fg-base">
                    {item.product_title}
                  </Text>
                  {item.variant_title && (
                    <Text className="text-ui-fg-muted text-sm">
                      Variant: {item.variant_title}
                    </Text>
                  )}
                  <Text className="text-ui-fg-muted text-sm">
                    Quantity: {item.quantity}
                  </Text>
                </div>
              </div>

              {/* Custom Fields */}
              {item.metadata?.custom_fields && item.metadata.custom_fields.length > 0 && (
                <div className="mb-4 p-3 bg-ui-bg-field rounded-lg">
                  <Text className="font-medium text-ui-fg-base mb-2 txt-compact-medium">
                    Custom Fields
                  </Text>
                  <div className="space-y-1">
                    {item.metadata.custom_fields.map((field, index) => (
                      <div key={field.field_id || index} className="flex justify-between">
                        <Text className="text-ui-fg-subtle txt-compact-sm">
                          {field.name || `Field ${index + 1}`}
                        </Text>
                        <Text className="text-ui-fg-subtle txt-compact-sm">
                          {field.value}
                        </Text>
                      </div>
                    ))}
                  </div>
                </div>
              )}

              {/* Addon Products */}
              {addonItems.length > 0 && (
                <div className="p-3 bg-ui-bg-field rounded-lg">
                  <Text className="font-medium text-ui-fg-base mb-2 txt-compact-medium">
                    Add-on Products ({addonItems.length})
                  </Text>
                  <div className="space-y-2">
                    {addonItems.map((addon) => (
                      <div key={addon.id} className="flex justify-between items-center">
                        <div className="flex-1">
                          <Text className="text-ui-fg-base txt-compact-sm">
                            {addon.product_title}
                          </Text>
                          {addon.variant_title && (
                            <Text className="text-ui-fg-muted txt-compact-xs">
                              Variant: {addon.variant_title}
                            </Text>
                          )}
                          <Text className="text-ui-fg-muted txt-compact-sm">
                            Quantity: {addon.quantity}
                          </Text>
                        </div>
                      </div>
                    ))}
                  </div>
                </div>
              )}
            </div>
          )
        })}
      </div>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "order.details.side.after",
})

export default OrderBuilderDetailsWidget
```

You first define types for the line item of a product builder, and a type for its metadata.

Then, in the widget, you find the items that have builder configurations by checking if they have the `is_builder_main_product` metadata or custom fields.

If no builder items are found, the widget will not be displayed. Otherwise, you display the item's custom values and add-on products.

Notice that to find the addons of the main product, you compare the `main_product_line_item_id` of the addon with the `cart_line_item_id` of the main product's item.

### Test Order Admin Widget

To test out the widget on the order details page:

1. Make sure the Medusa Application is running.
2. Open the Medusa Admin dashboard and log in.
3. Go to Orders.
4. Click on an order that contains an item with builder configurations.

You'll find at the end of the side section an "Items with Builder Configurations" section. The section will show the custom field values and add-ons for each item that has builder configurations.

![Widget on the order details page showing the builder configurations for each item](https://res.cloudinary.com/dza7lstvk/image/upload/v1755169611/Medusa%20Resources/CleanShot_2025-08-14_at_14.06.40_2x_om7f6e.png)

***

## Next Steps

You've now implemented the product builder feature in Medusa. You can expand on this feature based on your use case. You can:

- Allow users to edit their product configurations from the cart or checkout page.
- Disallow purchasing addon products without a main product by filtering products with the `addon` tag.
- Expand on the builder configurations to support more complex setups.

### Learn More about Medusa

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.


# Implement Product Feed for Meta and Google

In this tutorial, you'll learn how to create a product feed in Medusa that can be used for Meta and Google.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) that are available out-of-the-box.

Businesses that are selling on social media platforms like Meta (Instagram and Facebook) and Google need to upload their product catalog to those platforms and keep them in sync with their Medusa store. Creating a product feed allows you to automate this process.

## Summary

By following this tutorial, you will learn how to:

- Install and set up Medusa with the Next.js Starter Storefront.
- Create a workflow that builds a product feed XML.
- Expose an API route to serve the product feed.
- Use the API route on social platforms like Meta and Google.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Products in the Meta Catalogue](https://res.cloudinary.com/dza7lstvk/image/upload/v1756732400/Medusa%20Resources/CleanShot_2025-09-01_at_16.12.33_2x_oszhkt.png)

- [Full Code](https://github.com/medusajs/examples/tree/main/product-feed): Find the full code for this tutorial in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1756719230/OpenApi/Product_Feed_qdma7g.yaml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.

Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Product Feed Workflow

In this step, you'll create the logic to build an XML string for a product feed.

In Medusa, you implement commerce logic within a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task. A workflow is similar to a function, but it allows you to track its executions' progress, define roll-back logic, and configure other advanced features.

You'll create a workflow that builds a product feed. Later, you'll execute the workflow from an API route, allowing third-party services to retrieve the product feed.

Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) documentation to learn more about workflows.

The workflow you'll create will have the following steps:

- [getProductFeedItemsStep](#getProductFeedItemsStep): Retrieve Medusa products as items for the feed.
- [buildProductFeedXmlStep](#buildProductFeedXmlStep): Get the product feed as an XML string.

### getProductFeedItemsStep

The `getProductFeedItemsStep` will retrieve the Medusa products with pagination, and format their product variants as items to be added to the feed.

To create the step, create the file `src/workflows/steps/get-product-feed-items.ts` with the following content:

```ts title="src/workflows/steps/get-product-feed-items.ts" highlights={getProductFeedItemsHighlights1}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { getVariantAvailability, QueryContext } from "@medusajs/framework/utils"
import { CalculatedPriceSet } from "@medusajs/framework/types"

export type FeedItem = {
  id: string
  title: string
  description: string
  link: string
  image_link?: string
  additional_image_link?: string
  availability: string
  price: string
  sale_price?: string
  item_group_id: string
  condition?: string
  brand?: string
}

const formatPrice = (price: number, currency_code: string) => {
  return `${new Intl.NumberFormat("en-US", {
    currency: currency_code,
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
  }).format(price)} ${currency_code.toUpperCase()}`
}

type StepInput = {
  currency_code: string
  country_code: string
}

export const getProductFeedItemsStep = createStep(
  "get-product-feed-items", 
  async (input: StepInput, { container }) => {
    // ...
  }
)
```

You first define a `FeedItem` type that represents each product in the feed. It has properties matching [Meta](https://www.facebook.com/business/help/120325381656392) and [Google](https://support.google.com/merchants/answer/7052112)'s specification. You can add other optional specification fields to this type, if necessary.

You also define a `formatPrice` function that will format a price with a currency code based on the format requested by Meta and Google. Meta and Google request that a price is formatted as "X.XX USD", where X.XX is the price with two decimal places, and `USD` is the currency code in uppercase.

After that, you create a step with the `createStep` function. It accepts two parameters:

1. The step's unique name.
2. An async function that receives two parameters:
   - The step's input, which is an object holding the requested currency and country codes.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.

Next, you'll implement the step's logic. Add the following in the step function:

```ts title="src/workflows/steps/get-product-feed-items.ts" highlights={getProductFeedItemsHighlights2}
const feedItems: FeedItem[] = []
const query = container.resolve("query")
const configModule = container.resolve("configModule")
const storefrontUrl = configModule.admin.storefrontUrl || 
  process.env.STOREFRONT_URL

const limit = 100
let offset = 0
let count = 0
const countryCode = input.country_code.toLowerCase()
const currencyCode = input.currency_code.toLowerCase()

do {
  const {
    data: products,
    metadata,
  } = await query.graph({
    entity: "product",
    fields: [
      "id",
      "title",
      "description",
      "handle",
      "thumbnail",
      "images.*",
      "status",
      "variants.*",
      "variants.calculated_price.*",
      "sales_channels.*",
      "sales_channels.stock_locations.*",
      "sales_channels.stock_locations.address.*",
    ],
    filters: {
      status: "published",
    },
    context: {
      variants: {
        calculated_price: QueryContext({
          currency_code: currencyCode,
        }),
      },
    },
    pagination: {
      take: limit,
      skip: offset,
    },
  })
  
  count = metadata?.count ?? 0
  offset += limit

  // TODO prepare feed data
} while (count > offset)

return new StepResponse({ items: feedItems })
```

You first initialize an empty array of `FeedItem` objects that you'll populate later.

Then, you resolve the following resources from the Medusa container:

- [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) that allows you to retrieve data across modules.
- [Medusa Configurations](https://docs.medusajs.com/docs/learn/configurations/medusa-config/index.html.md) that are defined in `medusa-config.ts`.

You use the Medusa configurations to retrieve the storefront URL, which you'll use to build the product links in the feed. If the storefront URL is not set in the configurations, you fall back to the `STOREFRONT_URL` environment variable.

After that, you use Query to retrieve product data with pagination. For each product, you retrieve fields and relations useful for the feed. You still need to add the logic for populating the feed items.

To retrieve the product variant prices for a currency code, you must pass the currency code as a [query context](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query-context/index.html.md). Learn more in the [Get Variant Prices](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price/index.html.md) chapter.

Finally, a step function must return a `StepResponse` instance with the step's output, which is the list of feed items.

#### Populate Feed Items

To populate the product data as feed items, replace the `TODO` in the step with the following:

```ts title="src/workflows/steps/get-product-feed-items.ts" highlights={getProductFeedItemsHighlights3}
for (const product of products) {
  if (!product.variants.length) {continue}
  const salesChannel = product.sales_channels?.find((channel) => {
    return channel?.stock_locations?.some((location) => {
      return location?.address?.country_code.toLowerCase() === countryCode
    })
  })

  const availability = salesChannel?.id ? await getVariantAvailability(query, {
    variant_ids: product.variants.map((variant) => variant.id),
    sales_channel_id: salesChannel?.id,
  }) : undefined

  for (const variant of product.variants) {
    // @ts-ignore
    const calculatedPrice = variant.calculated_price as CalculatedPriceSet
    const hasOriginalPrice = calculatedPrice?.original_amount
    const originalPrice = hasOriginalPrice ? calculatedPrice.original_amount : 
    calculatedPrice.calculated_amount
    const salePrice = hasOriginalPrice ? calculatedPrice.calculated_amount : 
      undefined
    const stockStatus = !variant.manage_inventory ? "in stock" : 
      !availability?.[variant.id]?.availability ? "out of stock" : "in stock"

    feedItems.push({
      id: variant.id,
      title: product.title,
      description: product.description ?? "",
      link: `${storefrontUrl || ""}/${input.country_code}/${product.handle}`,
      image_link: product.thumbnail ?? "",
      additional_image_link: product.images?.map(
        (image) => image.url
      )?.join(","),
      availability: stockStatus,
      price: formatPrice(originalPrice as number, currencyCode),
      sale_price: salePrice ? formatPrice(salePrice as number, currencyCode) : 
        undefined,
      item_group_id: product.id,
      condition: "new", // TODO add condition if supported
      brand: "", // TODO add brand if supported
    })
  }
}
```

For each product, you:

- Skip the product if it doesn't have variants.
  - In Medusa, customers purchase variants of a product.
- Try to retrieve the sales channel of a product that has stock locations in the requested country code.
  - In Medusa, a product variant's inventory is tracked by stock locations that are associated with sales channels. So, you must retrieve the product's sales channel that matches the requested country code to check the availability of each variant. Learn more in the [Product Variant Inventory](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md) guide.
- For each variant, you:
  - Retrieve the variant's price and sale price.
    - If the `calculated_price.original_amount` is different than `calculated_price.calculated_amount`, the variant is on sale and the `calculated_price.calculated_amount` is the sale price.
    - Otherwise, the `calculated_price.calculated_amount` is the regular price.
  - Check the variant's availability.
    - If the variant's `manage_inventory` property is disabled, the variant is always in stock.
    - If the variant's `manage_inventory` property is enabled, check the availability retrieved from the `getVariantAvailability` function.
  - Populate the feed item with the variant's data.

The `feedItems` array will contain feed data for every product variant.

### buildProductFeedXmlStep

In the `buildProductFeedXmlStep`, you will construct the XML string for the product feed.

To create the step, create the file `src/workflows/steps/build-product-feed-xml.ts` with the following content:

```ts title="src/workflows/steps/build-product-feed-xml.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { FeedItem } from "./get-product-feed-items"

type StepInput = {
  items: FeedItem[]
}

export const buildProductFeedXmlStep = createStep(
  "build-product-feed-xml",
  async (input: StepInput) => {
    const escape = (str: string) =>
      str
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/\"/g, "&quot;")
        .replace(/'/g, "&apos;")

    const itemsXml = input.items.map((item) => {
      return (
        `<item>` +
          `<g:id>${escape(item.id)}</g:id>` +
          `<title>${escape(item.title)}</title>` +
          `<description>${escape(item.description)}</description>` +
          `<link>${escape(item.link)}</link>` +
          (item.image_link ? `<g:image_link>${escape(item.image_link)}</g:image_link>` : "") +
          (item.additional_image_link ? `<g:additional_image_link>${escape(item.additional_image_link)}</g:additional_image_link>` : "") +
          `<g:availability>${escape(item.availability)}</g:availability>` +
          `<g:price>${escape(item.price)}</g:price>` +
          (item.sale_price ? `<g:sale_price>${escape(item.sale_price)}</g:sale_price>` : "") +
          `<g:condition>${escape(item.condition || "new")}</g:condition>` +
          (item.brand ? `<g:brand>${escape(item.brand)}</g:brand>` : "") +
          `<g:item_group_id>${escape(item.item_group_id)}</g:item_group_id>` +
        `</item>`
      )
    }).join("")

    const xml =
      `<?xml version="1.0" encoding="UTF-8"?>` +
      `<rss version="2.0" xmlns:g="http://base.google.com/ns/1.0">` +
        `<channel>` +
          `<title>Product Feed</title>` +
          `<description>Product Feed for Social Platforms</description>` +
          itemsXml +
        `</channel>` +
      `</rss>`

    return new StepResponse(xml)
  }
)
```

This step accepts the feed items as an input.

In the step, you format the XML string based on the specifications accepted by Meta and Google. You also escape special characters in the feed data to ensure the XML is valid.

Finally, the step returns the XML string.

### Create the Workflow

Now that you have the steps, you can create the workflow to build a product feed.

Create the file `src/workflows/generate-product-feed.ts` with the following content:

```ts title="src/workflows/generate-product-feed.ts"
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { getProductFeedItemsStep } from "./steps/get-product-feed-items"
import { buildProductFeedXmlStep } from "./steps/build-product-feed-xml"

type GenerateProductFeedWorkflowInput = {
  currency_code: string
  country_code: string
}

export const generateProductFeedWorkflow = createWorkflow(
  "generate-product-feed",
  (input: GenerateProductFeedWorkflowInput) => {
    const { items: feedItems } = getProductFeedItemsStep(input)

    const xml = buildProductFeedXmlStep({ 
      items: feedItems,
    })

    return new WorkflowResponse({ xml })
  }
)

export default generateProductFeedWorkflow
```

You create a workflow using the `createWorkflow` function. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function that holds the workflow's implementation.

The constructor function accepts an object holding the currency code and country code.

In the function, you:

- Get the product feed items using the `getProductFeedItemsStep`.
- Build the product feed XML using the `buildProductFeedXmlStep`.

A workflow must return an instance of `WorkflowResponse`. It receives as a parameter the data returned by the workflow, which is the XML string.

In the next step, you'll create an API route that executes this workflow.

***

## Step 3: Create Product Feed API Route

In this step, you'll expose the product feed XML by creating an API route.

An [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) is an endpoint that exposes commerce features to external applications and clients, such as third-party services.

Refer to the [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) documentation to learn more.

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

So, to create the API route, create the file `src/api/product-feed/route.ts` with the following content:

```ts title="src/api/product-feed/route.ts"
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import generateProductFeedWorkflow from "../../workflows/generate-product-feed"

export async function GET(
  req: MedusaRequest, 
  res: MedusaResponse
) {
  const { 
    currency_code,
    country_code,
   } = req.validatedQuery

  const { result } = await generateProductFeedWorkflow(req.scope).run({
    input: {
      currency_code: currency_code as string,
      country_code: country_code as string,
    },
  })

  res.setHeader("Content-Type", "application/rss+xml; charset=utf-8")
  res.status(200).send(result.xml)
}
```

By exporting a `GET` route handler function, you expose a `GET` API route at the `/product-feed` path.

In the route handler, you retrieve the country and currency codes from the request's query parameters.

Then, you execute the `generateProductFeedWorkflow` by invoking it, passing it the Medusa container (which is `req.scope`), and calling its `run` method. You pass the workflow's input to the `run` method.

Finally, you set the response headers to indicate that the content is XML and send the XML string in the response.

### Add Query Validation Middleware

To ensure that the country and currency codes are passed as query parameters, you need to apply a middleware.

A [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) is a function that runs when a request is sent before running the route handler. It's useful to validate request query and body parameters.

To apply a middleware on the API route, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import { defineMiddlewares, validateAndTransformQuery } from "@medusajs/framework/http"
import { z } from "zod"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/product-feed",
      methods: ["GET"],
      middlewares: [
        validateAndTransformQuery(z.object({
          currency_code: z.string(),
          country_code: z.string(),
        }), {}),
      ],
    },
  ],
})
```

You apply the `validateAndTransformQuery` middleware on the `/product-feed` API route. The middleware accepts as a first parameter a [Zod](https://zod.dev/) schema that defines the expected query parameters.

The request will now fail before reaching the route handler if the query parameters are invalid.

### Test it Out

To test out the product feed API route, start the Medusa server with the following command:

```bash npm2yarn
npm run dev
```

Then, in your browser, go to `http://localhost:9000/product-feed?currency_code=eur&country_code=dk`. You can replace the currency and country codes based on the ones you use in your store.

You'll receive an XML in the response similar to the following:

```plain
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:g="http://base.google.com/ns/1.0">
  <channel>
    <title>Product Feed</title>
    <description>Product Feed for Social Platforms</description>
    <item>
      <g:id>variant_123</g:id>
      <g:title>Product Title</g:title>
      <g:description>Product Description</g:description>
      <g:link>https://example.com/dk/product-handle</g:link>
      <g:image_link>https://example.com/product/image.jpg</g:image_link>
      <g:price>19.99 EUR</g:price>
      <g:availability>in stock</g:availability>
      <g:item_group_id>product_123</g:item_group_id>
    </item>
  </channel>
</rss>
```

***

## Step 4: Use the Product Feed

If your Medusa application is [deployed](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/deployment/index.html.md), you can now use the product feed API route on social platforms like [Meta](https://www.facebook.com/business/help/125074381480892?id=725943027795860) and [Google](https://support.google.com/merchants/answer/11586438?hl=en\&sjid=6331702527227918188-EU).

Make sure to set the [admin.storefrontUrl](https://docs.medusajs.com/docs/learn/configurations/medusa-config#storefronturl/index.html.md) or `STOREFRONT_URL` environment variable before using the product feed API route.

For example, to add your product feed as a data source in Meta:

### Prerequisites

- [Meta Business Portfolio](https://www.facebook.com/business/help/1710077379203657)
- [Shop or Catalogue in Business Portfolio](https://www.facebook.com/business/help/1275400645914358?id=725943027795860)

1. Go to your [Meta Business Portfolio](https://business.facebook.com/).
2. Select your business portfolio.
3. Go to Catalogue -> Data sources.
4. Choose the "Data feed" option, and click Next.

![Meta Data Sources Setup Form](https://res.cloudinary.com/dza7lstvk/image/upload/v1756731936/Medusa%20Resources/CleanShot_2025-09-01_at_16.04.51_2x_kht1lx.png)

5. Choose the "Use a URL or Google Sheets" option, and enter the URL to the product feed API route. For example, `https://your-medusa-store.com/product-feed?currency_code=eur&country_code=dk`.
6. Click the Next button.
7. In the pop-up that opens, choose "EUR" as the default currency, or the currency you want to use.
8. Click the Upload button.

Then, wait until Meta finishes processing and uploading your products. Once it's done, you can view the products in Catalogue -> Products.

![Meta Catalogue Products](https://res.cloudinary.com/dza7lstvk/image/upload/v1756732208/Medusa%20Resources/CleanShot_2025-09-01_at_16.09.52_2x_tbs52f.png)

***

## Next Steps

You've now set up the product feed API route for your Medusa application. Meta and Google will pull products from this feed periodically, ensuring your product listings are always up to date.

You can add more fields to the product feed based on your use case. Refer to [Meta](https://www.facebook.com/business/help/120325381656392) and [Google](https://support.google.com/merchants/answer/7052112)'s product feed specifications for more details on available fields and their formats.

### Learn More about Medusa

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.


# Implement Product Rentals in Medusa

In this tutorial, you'll learn how to implement product rentals in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) that are available out-of-the-box.

Product rentals allow customers to rent products for a specified period. This feature is particularly useful for businesses that offer items like equipment, vehicles, or formal wear.

## Summary

By following this tutorial, you'll learn how to:

- Install and set up Medusa with the Next.js Starter Storefront.
- Define and manage data models useful for product rentals.
- Allow admin users to manage rental configurations of products.
- Allow customers to rent products for specified periods through the storefront.
- Allow admin users to manage rented items in orders.
- Handle events like order cancellation and fulfillment for rented products.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Diagram of the Rentals Module and its connection with Medusa's Product, Order, and Customer Modules](https://res.cloudinary.com/dza7lstvk/image/upload/v1761722702/Medusa%20Resources/product-rentals-summary_rhjwjn.jpg)

- [Full Code](https://github.com/medusajs/examples/tree/main/product-rentals): Find the full code for this tutorial in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1761669774/OpenApi/product-rentals_z0csl5.yaml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.

Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Rental Module

In Medusa, you can build custom features in a [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a reusable package with the data models and functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more.

In this step, you'll build a Rental Module that defines the data models and logic to manage rentals and rental configurations in the database.

### a. Create Module Directory

Create the directory `src/modules/rental` that will hold the Rental Module's code.

### b. Create Data Models

A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

Refer to the [Data Models](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md) documentation to learn more.

For the Rental Module, you'll create a data model to represent rental configurations for products, and another to represent individual rentals.

#### RentalConfiguration Data Model

The `RentalConfiguration` data model holds rental configurations for products. Products with a rental configuration can be rented.

To create the data model, create the file `src/modules/rental/models/rental-configuration.ts` with the following content:

```ts title="src/modules/rental/models/rental-configuration.ts"
import { model } from "@medusajs/framework/utils"
import { Rental } from "./rental"

export const RentalConfiguration = model.define("rental_configuration", {
  id: model.id().primaryKey(),
  product_id: model.text(),
  min_rental_days: model.number().default(1),
  max_rental_days: model.number().nullable(),
  status: model.enum(["active", "inactive"]).default("active"),
  rentals: model.hasMany(() => Rental, {
    mappedBy: "rental_configuration",
  }),
})
```

The `RentalConfiguration` data model has the following properties:

- `id`: The primary key of the table.
- `product_id`: The ID of the Medusa product associated with the rental configuration.
- `min_rental_days`: The minimum number of days a product can be rented.
- `max_rental_days`: The maximum number of days a product can be rented.
- `status`: The status of the rental configuration, which can be either "active" or "inactive".
- `rentals`: A one-to-many relation to the `Rental` data model, which you'll create next.

Notice that you'll handle pricing and inventory through Medusa's existing [Product](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md) and [Inventory](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) modules.

Learn more about defining data model properties in the [Property Types documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md).

#### Rental Data Model

The `Rental` data model holds individual rentals. They will be created for each rented product variant in an order.

To create the data model, create the file `src/modules/rental/models/rental.ts` with the following content:

```ts title="src/modules/rental/models/rental.ts"
import { model } from "@medusajs/framework/utils"
import { RentalConfiguration } from "./rental-configuration"

export const Rental = model.define("rental", {
  id: model.id().primaryKey(),
  variant_id: model.text(),
  customer_id: model.text(),
  order_id: model.text().nullable(),
  line_item_id: model.text().nullable(),
  rental_start_date: model.dateTime(),
  rental_end_date: model.dateTime(),
  actual_return_date: model.dateTime().nullable(),
  rental_days: model.number(),
  status: model.enum(["pending", "active", "returned", "cancelled"]).default("pending"),
  rental_configuration: model.belongsTo(() => RentalConfiguration, {
    mappedBy: "rentals",
  }),
})
```

The `Rental` data model has the following properties:

- `id`: The primary key of the table.
- `variant_id`: The ID of the Medusa product variant being rented.
- `customer_id`: The ID of the customer renting the product.
- `order_id`: The ID of the Medusa order associated with the rental.
- `line_item_id`: The ID of the Medusa line item associated with the rental.
- `rental_start_date`: The start date of the rental period.
- `rental_end_date`: The end date of the rental period.
- `actual_return_date`: The actual return date of the rented product.
- `rental_days`: The number of days the product is rented.
- `status`: The status of the rental, which can be "pending", "active", "returned", or "cancelled".
- `rental_configuration`: A many-to-one relation to the `RentalConfiguration` data model.

### c. Create Module's Service

You can manage your module's data models in a service.

A service is a TypeScript class that the module exports. In the service's methods, you can connect to the database, allowing you to manage your data models, or connect to a third-party service, which is useful if you're integrating with external services.

Refer to the [Module Service documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md) to learn more.

To create the Rental Module's service, create the file `src/modules/rental/service.ts` with the following content:

```ts title="src/modules/rental/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import { Rental } from "./models/rental"
import { RentalConfiguration } from "./models/rental-configuration"

class RentalModuleService extends MedusaService({
  Rental,
  RentalConfiguration,
}) {
  
}

export default RentalModuleService
```

The `RentalModuleService` extends `MedusaService`, which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods.

So, the `RentalModuleService` class now has methods like `createRentals` and `retrieveRentalConfiguration`.

Find all methods generated by the `MedusaService` in [the Service Factory](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md) reference.

### d. Create the Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/rental/index.ts` with the following content:

```ts title="src/modules/rental/index.ts"
import RentalModuleService from "./service"
import { Module } from "@medusajs/framework/utils"

export const RENTAL_MODULE = "rental"

export default Module(RENTAL_MODULE, {
  service: RentalModuleService,
})
```

You use the `Module` function to create the module's definition. It accepts two parameters:

1. The module's name, which is `rental`.
2. An object with a required `service` property indicating the module's service.

You also export the module's name as `RENTAL_MODULE` so you can reference it later.

### e. Add Module to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property and pass it an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/rental",
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

### f. Generate Migrations

Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript class that defines database changes made by a module.

Refer to the [Migrations documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md) to learn more.

Medusa's CLI tool can generate the migrations for you. To generate a migration for the Rental Module, run the following command in your Medusa application's directory:

```bash
npx medusa db:generate rental
```

The `db:generate` command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a `migrations` directory under `src/modules/rental` that holds the generated migration.

Then, to reflect these migrations on the database, run the following command:

```bash
npx medusa db:migrate
```

The tables for the data models are now created in the database.

***

## Step 3: Define Links between Data Models

Since Medusa isolates modules to integrate them into your application without side effects, you can't directly create relationships between data models of different modules.

Instead, Medusa provides a mechanism to define links between data models and retrieve and manage linked records while maintaining module isolation.

Refer to the [Module Links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md) documentation to learn more about defining links.

In this step, you'll define links between the data models in the Rental Module and the data models in Medusa's modules:

1. `RentalConfiguration` ↔ `Product`: The rental configuration of a product.
2. `Rental` -> `Order`: The order that the rental belongs to.
3. `Rental` -> `OrderLineItem`: The associated order line item for the rental.
4. `Customer` -> `Rental`: The customer who made the rental.
5. `Rental` -> `ProductVariant`: The product variant being rented.

### a. RentalConfiguration ↔ Product Link

To define the link between `RentalConfiguration` and `Product`, create the file `src/links/product-rental-config.ts` with the following content:

```ts title="src/links/product-rental-config.ts"
import { defineLink } from "@medusajs/framework/utils"
import RentalModule from "../modules/rental"
import ProductModule from "@medusajs/medusa/product"

export default defineLink(
  ProductModule.linkable.product,
  RentalModule.linkable.rentalConfiguration
)
```

You define a link using the `defineLink` function. It accepts two parameters:

1. An object indicating the first data model part of the link. A module has a special `linkable` property that contains link configurations for its data models. You pass the linkable configurations of the Product Module's `Product` data model.
2. An object indicating the second data model part of the link. You pass the linkable configurations of the Rental Module's `RentalConfiguration` data model.

### b. Rental -> Order Link

To define the link from a `Rental` to an `Order`, create the file `src/links/rental-order.ts` with the following content:

```ts title="src/links/rental-order.ts"
import { defineLink } from "@medusajs/framework/utils"
import RentalModule from "../modules/rental"
import OrderModule from "@medusajs/medusa/order"

export default defineLink(
  {
    linkable: RentalModule.linkable.rental,
    field: "order_id",
  },
  OrderModule.linkable.order,
  {
    readOnly: true,
  }
)
```

You define a link similar to the previous one, but with an additional configuration object as the third parameter. You enable the `readOnly` property to indicate that this link is not saved in the database. It's only used to query the order of a rental.

### c. Rental -> OrderLineItem Link

To define the link from a `Rental` to an `OrderLineItem`, create the file `src/links/rental-line-item.ts` with the following content:

```ts title="src/links/rental-line-item.ts"
import { defineLink } from "@medusajs/framework/utils"
import RentalModule from "../modules/rental"
import OrderModule from "@medusajs/medusa/order"

export default defineLink(
  {
    linkable: RentalModule.linkable.rental,
    field: "line_item_id",
  },
  OrderModule.linkable.orderLineItem,
  {
    readOnly: true,
  }
)
```

You define the link similarly to the previous one, enabling the `readOnly` property to indicate that this link is not saved in the database. It's only used to query the line item of a rental.

### d. Customer -> Rental Link

To define the link from a `Customer` to a `Rental`, create the file `src/links/rental-customer.ts` with the following content:

```ts title="src/links/rental-customer.ts"
import { defineLink } from "@medusajs/framework/utils"
import RentalModule from "../modules/rental"
import CustomerModule from "@medusajs/medusa/customer"

export default defineLink(
  {
    linkable: CustomerModule.linkable.customer,
    field: "id",
  },
  {
    ...RentalModule.linkable.rental.id,
    primaryKey: "customer_id",
  },
  {
    readOnly: true,
  }
)
```

You define the link similarly to the previous ones, enabling the `readOnly` property to indicate that this link is not saved in the database. It's only used to query the customer of a rental.

### e. Rental -> ProductVariant Link

To define the link from a `Rental` to a `ProductVariant`, create the file `src/links/rental-variant.ts` with the following content:

```ts title="src/links/rental-variant.ts"
import { defineLink } from "@medusajs/framework/utils"
import RentalModule from "../modules/rental"
import ProductModule from "@medusajs/medusa/product"

export default defineLink(
  {
    linkable: RentalModule.linkable.rental,
    field: "variant_id",
  },
  ProductModule.linkable.productVariant,
  {
    readOnly: true,
  }
)
```

You define the link similarly to the previous ones, enabling the `readOnly` property to indicate that this link is not saved in the database. It's only used to query the product variant of a rental.

### f. Sync Links to Database

After defining links, you need to sync them to the database. This creates the necessary tables to store the link between the `RentalConfiguration` and `Product` data models.

To sync the links to the database, run the migrations command again in the Medusa application's directory:

```bash
npx medusa db:migrate
```

This command will create the necessary table to store the link. The other links are read-only and don't require database changes.

***

## Step 4: Manage Rental Configurations Workflow

In this step, you'll implement the logic to create or update a rental configuration for a product. Later, you'll execute this logic from an API route, and allow admin users to manage rental configurations from the Medusa Admin dashboard.

You create custom functionalities in [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task. A workflow is similar to a function, but it allows you to track its executions' progress, define roll-back logic, and configure other advanced features.

Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) documentation to learn more.

The workflow to manage rental configurations will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve product's rental configuration details.

Medusa provides the `useQueryGraphStep` and `createRemoteLinkStep` out-of-the-box. So, you only need to implement the other steps.

### createRentalConfigurationStep

The `createRentalConfigurationStep` creates a rental configuration.

To create the step, create the file `src/workflows/steps/create-rental-configuration.ts` with the following content:

```ts title="src/workflows/steps/create-rental-configuration.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { RENTAL_MODULE } from "../../modules/rental"
import RentalModuleService from "../../modules/rental/service"

type CreateRentalConfigurationInput = {
  product_id: string
  min_rental_days?: number
  max_rental_days?: number | null
  status?: "active" | "inactive"
}

export const createRentalConfigurationStep = createStep(
  "create-rental-configuration",
  async (
    input: CreateRentalConfigurationInput,
    { container }
  ) => {
    const rentalModuleService: RentalModuleService = container.resolve(
      RENTAL_MODULE
    )

    const rentalConfig = await rentalModuleService.createRentalConfigurations(
      input
    )

    return new StepResponse(rentalConfig, rentalConfig.id)
  },
  async (rentalConfigId, { container }) => {
    if (!rentalConfigId) {return}

    const rentalModuleService: RentalModuleService = container.resolve(
      RENTAL_MODULE
    )

    // Delete the created configuration on rollback
    await rentalModuleService.deleteRentalConfigurations(rentalConfigId)
  }
)
```

You create a step with the `createStep` function. It accepts three parameters:

1. The step's unique name.
2. An async function that receives two parameters:
   - The step's input, which is an object with the rental configuration's details.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.
3. An async compensation function that undoes the actions performed by the step function. This function is only executed if an error occurs during the workflow's execution.

In the step function, you resolve the Rental Module's service from the Medusa container, and use it to create a rental configuration.

A step function must return a `StepResponse` instance with the step's output as a first parameter, and the data to pass to the compensation function as a second parameter.

In the compensation function, you delete the created rental configuration if an error occurs during the workflow's execution.

### updateRentalConfigurationStep

The `updateRentalConfigurationStep` updates a rental configuration.

To create the step, create the file `src/workflows/steps/update-rental-configuration.ts` with the following content:

```ts title="src/workflows/steps/update-rental-configuration.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { RENTAL_MODULE } from "../../modules/rental"
import RentalModuleService from "../../modules/rental/service"

type UpdateRentalConfigurationInput = {
  id: string
  min_rental_days?: number
  max_rental_days?: number | null
  status?: "active" | "inactive"
}

export const updateRentalConfigurationStep = createStep(
  "update-rental-configuration",
  async (
    input: UpdateRentalConfigurationInput,
    { container }
  ) => {
    const rentalModuleService: RentalModuleService = container.resolve(
      RENTAL_MODULE
    )

    // retrieve existing rental configuration
    const existingRentalConfig = await rentalModuleService.retrieveRentalConfiguration(
      input.id
    )

    const updatedRentalConfig = await rentalModuleService.updateRentalConfigurations(
      input
    )

    return new StepResponse(updatedRentalConfig, existingRentalConfig)
  },
  async (existingRentalConfig, { container }) => {
    if (!existingRentalConfig) {return}

    const rentalModuleService: RentalModuleService = container.resolve(
      RENTAL_MODULE
    )

    await rentalModuleService.updateRentalConfigurations({
      id: existingRentalConfig.id,
      min_rental_days: existingRentalConfig.min_rental_days,
      max_rental_days: existingRentalConfig.max_rental_days,
      status: existingRentalConfig.status,
    })
  }
)
```

This step receives the rental configuration's ID and the details to update.

In the step, you retrieve the existing rental configuration before updating it. Then, you update the rental configuration using the Rental Module's service.

You return a `StepResponse` instance with the updated rental configuration as the output, and you pass the existing rental configuration to the compensation function.

In the compensation function, you revert the rental configuration to its previous state if an error occurs during the workflow's execution.

### Manage Rental Configuration Workflow

You can now create the workflow to manage rental configurations using the steps you created.

To create the workflow, create the file `src/workflows/upsert-rental-config.ts` with the following content:

```ts title="src/workflows/upsert-rental-config.ts"
import {
  createWorkflow,
  WorkflowResponse,
  transform,
  when,
} from "@medusajs/framework/workflows-sdk"
import { 
  useQueryGraphStep, 
  createRemoteLinkStep,
} from "@medusajs/medusa/core-flows"
import { Modules } from "@medusajs/framework/utils"
import { 
  createRentalConfigurationStep,
} from "./steps/create-rental-configuration"
import { 
  updateRentalConfigurationStep,
} from "./steps/update-rental-configuration"
import { 
  RENTAL_MODULE,
} from "../modules/rental"

type UpsertRentalConfigWorkflowInput = {
  product_id: string
  min_rental_days?: number
  max_rental_days?: number | null
  status?: "active" | "inactive"
}

export const upsertRentalConfigWorkflow = createWorkflow(
  "upsert-rental-config",
  (input: UpsertRentalConfigWorkflowInput) => {
    // Retrieve product with its rental configuration
    const { data: products } = useQueryGraphStep({
      entity: "product",
      fields: ["id", "rental_configuration.*"],
      filters: { id: input.product_id },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    // If rental config doesn't exist, create it and link
    const createdConfig = when({ products }, (data) => {
      return !data.products[0]?.rental_configuration
    }).then(() => {
      const newConfig = createRentalConfigurationStep({
        product_id: input.product_id,
        min_rental_days: input.min_rental_days,
        max_rental_days: input.max_rental_days,
        status: input.status,
      })

      // Create link between product and rental configuration
      const linkData = transform({ 
        newConfig, 
        product_id: input.product_id,
      }, (data) => {
        return [
          {
            [Modules.PRODUCT]: {
              product_id: data.product_id,
            },
            [RENTAL_MODULE]: {
              rental_configuration_id: data.newConfig.id,
            },
          },
        ]
      })

      createRemoteLinkStep(linkData)

      return newConfig
    })

    // If rental config exists, update it
    // @ts-ignore
    const updatedConfig = when({ products }, (data) => {
      return !!data.products[0]?.rental_configuration
    }).then(() => {
      return updateRentalConfigurationStep({
        id: products[0].rental_configuration!.id,
        min_rental_days: input.min_rental_days,
        max_rental_days: input.max_rental_days,
        status: input.status,
      })
    })

    // Return whichever config was created or updated
    const rentalConfig = transform({ updatedConfig, createdConfig }, (data) => {
      return data.updatedConfig || data.createdConfig
    })

    return new WorkflowResponse(rentalConfig)
  }
)
```

You create a workflow using the `createWorkflow` function. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function that holds the workflow's implementation.

The function accepts an input object holding the rental configuration's details.

In the workflow, you:

1. Retrieve the product's details with its rental configuration using the `useQueryGraphStep`. This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) under the hood, which retrieves data across modules.
   - You enable the `throwIfKeyNotFound` option to throw an error if the product doesn't exist.
2. Use [when-then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) to check if the product doesn't have a rental configuration. If true, you:
   - Create a rental configuration using the `createRentalConfigurationStep`.
   - Create a link between the product and the created rental configuration using the `createRemoteLinkStep`.
3. Use [when-then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) to check if the product has a rental configuration. If true, you update the rental configuration using the `updateRentalConfigurationStep`.
4. Prepare the data to return using [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) to return either the created or updated rental configuration.

A workflow must return a `WorkflowResponse` instance with the workflow's output. You return the created or updated rental configuration.

You'll execute this workflow from an API route in the next step.

In workflows, you need `transform` and `when-then` to perform operations or check conditions based on execution values. Learn more in the [Conditions](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) and [Data Manipulation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) workflow documentation.

***

## Step 5: Manage Rental Configurations API Route

In this step, you'll create API routes that allow you to retrieve and manage rental configurations. Later, you'll use these API routes in the Medusa Admin dashboard to allow admin users to manage rental configurations.

### a. Manage Rental Configurations API Route

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

Refer to the [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) documentation to learn more about them.

To create an API route that upserts rental configurations of a product, create the file `src/api/admin/products/[id]/rental-config/route.ts` with the following content:

```ts title="src/api/admin/products/[id]/rental-config/route.ts"
import type { 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { 
  upsertRentalConfigWorkflow,
} from "../../../../../workflows/upsert-rental-config"
import { z } from "zod"

export const PostRentalConfigBodySchema = z.object({
  min_rental_days: z.number().optional(),
  max_rental_days: z.number().nullable().optional(),
  status: z.enum(["active", "inactive"]).optional(),
})

export const POST = async (
  req: MedusaRequest<z.infer<typeof PostRentalConfigBodySchema>>,
  res: MedusaResponse
) => {
  const { id } = req.params

  const { result } = await upsertRentalConfigWorkflow(req.scope).run({
    input: {
      product_id: id,
      min_rental_days: req.validatedBody.min_rental_days,
      max_rental_days: req.validatedBody.max_rental_days,
      status: req.validatedBody.status,
    },
  })

  res.json({ rental_config: result })
}
```

You first define a [Zod](https://zod.dev/) schema to validate the request body.

Then, since you export a `POST` function, you expose a `POST` API route at `/admin/products/:id/rental-config`.

In the API route handler, you execute the `upsertRentalConfigWorkflow` by invoking it, passing it the Medusa container from the request's scope. Then, you call its `run` method, passing the workflow's input from the request's parameters and validated body.

Finally, you return the created or updated rental configuration in the response.

### b. Add Validation Middleware

To validate the body parameters of requests sent to the API route, you need to apply a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

To apply a middleware, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares, 
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { 
  PostRentalConfigBodySchema,
} from "./admin/products/[id]/rental-config/route"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/admin/products/:id/rental-config",
      methods: ["POST"],
      middlewares: [
        validateAndTransformBody(PostRentalConfigBodySchema),
      ],
    },
  ],
})
```

You apply the `validateAndTransformBody` middleware to the `POST` route of the `/admin/products/:id/rental-config` path, passing it the Zod schema you created in the route file.

Any request that doesn't conform to the schema will receive a `400` Bad Request response.

Refer to the [Middlewares](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) documentation to learn more.

### c. Retrieve Rental Configuration API Route

Next, you'll add an API route at the same path to retrieve a product's rental configuration.

In `src/api/admin/products/[id]/rental-config/route.ts`, add the following at the end of the file:

```ts title="src/api/admin/products/[id]/rental-config/route.ts"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
  const { id } = req.params
  const query = req.scope.resolve("query")

  // Query rental configuration for the product
  const { data: rentalConfigs } = await query.graph({
    entity: "rental_configuration",
    fields: ["*"],
    filters: { product_id: id },
  })

  res.json({ rental_config: rentalConfigs[0] })
}
```

You expose a `GET` API route at `/admin/products/:id/rental-config`. In the route handler, you resolve [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) from the Medusa container and use it to query the rental configuration for the product.

Then, you return the rental configuration in the response.

In the next step, you'll consume these API routes in the Medusa Admin dashboard.

***

## Step 6: Manage Rental Configurations in Medusa Admin

In this step, you'll customize the Medusa Admin dashboard to allow admin users to manage rental configurations for products.

The Medusa Admin dashboard is customizable, allowing you to insert widgets into existing pages, or create new pages.

Refer to the [Admin Development](https://docs.medusajs.com/docs/learn/fundamentals/admin/index.html.md) documentation to learn more.

In this step, you'll insert a widget into the Product Details page to allow admin users to manage rental configurations for products.

### a. Initialize JS SDK

To send requests to the Medusa server, you'll use the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md). It's already installed in your Medusa project, but you need to initialize it before using it in your customizations.

Create the file `src/admin/lib/sdk.ts` with the following content:

```ts title="src/admin/lib/sdk.ts" highlights={sdkHighlights}
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: process.env.MEDUSA_BACKEND_URL || "http://localhost:9000",
  debug: process.env.NODE_ENV === "development",
  auth: {
    type: "session",
  },
})
```

Learn more about the initialization options in the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) reference.

### b. Create Rental Configuration Widget

Next, you'll create the widget to manage rental configurations on the Product Details page.

To create the widget, create the file `src/admin/widgets/product-rental-config.tsx` with the following content:

```tsx title="src/admin/widgets/product-rental-config.tsx" collapsibleLines="1-18" expandButtonLabel="Show Imports"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import {
  Container,
  Heading,
  Text,
  Button,
  Drawer,
  Input,
  Label,
  toast,
  Badge,
  usePrompt,
} from "@medusajs/ui"
import { useQuery, useMutation } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"
import { DetailWidgetProps, AdminProduct } from "@medusajs/framework/types"
import { useEffect, useState } from "react"

type RentalConfig = {
  id: string
  product_id: string
  min_rental_days: number
  max_rental_days: number | null
  status: "active" | "inactive"
}

type RentalConfigResponse = {
  rental_config: RentalConfig | null
}

const ProductRentalConfigWidget = ({
  data: product,
}: DetailWidgetProps<AdminProduct>) => {
  // TODO implement component
}

export const config = defineWidgetConfig({
  zone: "product.details.after",
})

export default ProductRentalConfigWidget
```

A widget file must export:

- A default React component. This component renders the widget's UI.
- A `config` object created with `defineWidgetConfig` from the Admin SDK. It accepts an object with the `zone` property that indicates where the widget will be rendered in the Medusa Admin dashboard.

Next, you'll add the implementation of the `ProductRentalConfigWidget` component.

Replace the `// TODO implement component` comment with the following code inside the `ProductRentalConfigWidget` component:

```tsx title="src/admin/widgets/product-rental-config.tsx"
const [drawerOpen, setDrawerOpen] = useState(false)
const [minRentalDays, setMinRentalDays] = useState(1)
const [maxRentalDays, setMaxRentalDays] = useState<number | null>(null)
const confirm = usePrompt()

const { data, isLoading, refetch } = useQuery<RentalConfigResponse>({
  queryFn: () =>
    sdk.client.fetch(`/admin/products/${product.id}/rental-config`),
  queryKey: [["products", product.id, "rental-config"]],
})

const upsertMutation = useMutation({
  mutationFn: async (config: {
    min_rental_days: number
    max_rental_days: number | null
    status?: "active" | "inactive"
  }) => {
    return sdk.client.fetch(`/admin/products/${product.id}/rental-config`, {
      method: "POST",
      body: config,
    })
  },
  onSuccess: () => {
    toast.success("Rental configuration updated successfully")
    refetch()
    setDrawerOpen(false)
  },
  onError: () => {
    toast.error("Failed to update rental configuration")
  },
})

// TODO add useEffect + handle state changes
```

You define the following state variables and hooks:

1. `drawerOpen`, `minRentalDays`, and `maxRentalDays` state variables to manage the drawer visibility and form inputs.
2. `confirm` to show confirmation prompts using [Medusa's UI package](https://docs.medusajs.com/ui/components/prompt/index.html.md).
3. `data`, `isLoading`, and `refetch` from a `useQuery` hook to fetch the product's rental configuration using the `GET` API route you created earlier.
4. `upsertMutation` from a `useMutation` hook to upsert the rental configuration using the `POST` API route you created earlier.

Next, you need to handle data and state changes. Replace the `// TODO add useEffect + handle state changes` comment with the following code:

```tsx title="src/admin/widgets/product-rental-config.tsx"
useEffect(() => {
  if (data?.rental_config) {
    setMinRentalDays(data.rental_config.min_rental_days)
    setMaxRentalDays(data.rental_config.max_rental_days)
  }
}, [data?.rental_config])

const handleOpenDrawer = () => {
  setDrawerOpen(true)
}

const handleSubmit = () => {
  upsertMutation.mutate({
    min_rental_days: minRentalDays,
    max_rental_days: maxRentalDays,
  })
}

const handleToggleStatus = async () => {
  if (!data?.rental_config) {return}
  
  const newStatus = 
    data.rental_config.status === "active" ? 
      "inactive" : "active"
  const action = 
    newStatus === "inactive" ? "Deactivate" : "Activate"
  
  if (await confirm({
    title: `${action} rental configuration?`,
    description: `Are you sure you want to ${action.toLowerCase()} this rental configuration?`,
    variant: newStatus === "inactive" ? "danger" : "confirmation",
  })) {
    upsertMutation.mutate({
      status: newStatus,
    })
  }
}

// TODO render component
```

You add a `useEffect` hook to set the form inputs when the rental configuration data is fetched.

You also define the following functions:

- `handleOpenDrawer`: Opens the drawer that shows the rental configuration form.
- `handleSubmit`: Submits the form to upsert the rental configuration.
- `handleToggleStatus`: Toggles the rental configuration's status between `active` and `inactive`, showing a confirmation prompt before proceeding.

Finally, you'll implement the component's UI. Replace the `// TODO render component` comment with the following code:

```tsx title="src/admin/widgets/product-rental-config.tsx"
return (
  <>
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">Rental Configuration</Heading>
        {!isLoading && data?.rental_config && (
          <Badge color={data.rental_config.status === "active" ? "green" : "grey"} size="2xsmall">
            {data.rental_config.status === "active" ? "Active" : "Inactive"}
          </Badge>
        )}
      </div>

      {isLoading && (
        <div className="px-6 py-4">
          <Text className="text-ui-fg-subtle">Loading...</Text>
        </div>
      )}

      {!isLoading && !data?.rental_config && (
        <>
          <div className="px-6 py-4">
            <Text className="text-ui-fg-subtle">This product is not currently available for rental.</Text>
          </div>
          <div className="flex justify-end border-t px-6 py-4">
            <Button size="small" onClick={handleOpenDrawer} variant="secondary">
              Make Rentable
            </Button>
          </div>
        </>
      )}

      {!isLoading && data?.rental_config && (
        <div className="divide-y">
          <div className="grid grid-cols-2 items-center px-6 py-4">
            <Text size="small" weight="plus" className="mb-1">
              Min Rental Days
            </Text>
            <Text className="text-ui-fg-subtle text-right">{data.rental_config.min_rental_days}</Text>
          </div>
          <div className="grid grid-cols-2 items-center px-6 py-4">
            <Text size="small" weight="plus" className="mb-1">
              Max Rental Days
            </Text>
            <Text className="text-ui-fg-subtle text-right">
              {data.rental_config.max_rental_days ?? "Unlimited"}
            </Text>
          </div>
          <div className="flex gap-2 px-6 py-4 justify-end">
            <Button size="small" variant="secondary" onClick={handleOpenDrawer}>
              Edit
            </Button>
            <Button
              size="small"
              variant={"primary"}
              onClick={handleToggleStatus}
              disabled={upsertMutation.isPending}
              isLoading={upsertMutation.isPending}
            >
              {data.rental_config.status === "active" 
                ? "Deactivate" 
                : "Activate"
              }
            </Button>
          </div>
        </div>
      )}
    </Container>

    <Drawer open={drawerOpen} onOpenChange={setDrawerOpen}>
      <Drawer.Content>
        <Drawer.Header>
          <Drawer.Title>
            {data?.rental_config ? "Edit" : "Add"} Rental Configuration
          </Drawer.Title>
        </Drawer.Header>
        <Drawer.Body className="space-y-4">
          <div>
            <Label htmlFor="min_rental_days">Minimum Rental Days</Label>
            <Input
              id="min_rental_days"
              type="number"
              min="1"
              value={minRentalDays}
              onChange={(e) => setMinRentalDays(Number(e.target.value))}
            />
          </div>
          <div>
            <Label htmlFor="max_rental_days">
              Maximum Rental Days (leave empty for unlimited)
            </Label>
            <Input
              id="max_rental_days"
              type="number"
              min={minRentalDays}
              value={maxRentalDays ?? ""}
              onChange={(e) =>
                setMaxRentalDays(
                  e.target.value ? Number(e.target.value) : null
                )
              }
            />
          </div>
        </Drawer.Body>
        <Drawer.Footer>
          <div className="flex gap-2">
            <Button
              variant="secondary"
              onClick={() => setDrawerOpen(false)}
            >
              Cancel
            </Button>
            <Button
              onClick={handleSubmit}
              disabled={upsertMutation.isPending}
              isLoading={upsertMutation.isPending}
            >
              Save
            </Button>
          </div>
        </Drawer.Footer>
      </Drawer.Content>
    </Drawer>
  </>
)
```

You show a section with the rental configuration details if they exist. You also show a drawer with a form to create or update the rental configuration.

If the product has a rental configuration, you show a button to toggle its status between `active` and `inactive`.

### Test Rental Configuration Widget

You can now test the rental configuration widget in the Medusa Admin dashboard.

Run the following command in your Medusa application's directory to start the Medusa server:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard in your browser at `http://localhost:9000/app` and login with the user you created in the first step.

Navigate to the Products page, open any product's page, and scroll down to the Rental Configuration section. Click the "Make Rentable" button to set up the product's rental configuration.

![Rental Configuration Widget](https://res.cloudinary.com/dza7lstvk/image/upload/v1761650341/Medusa%20Resources/CleanShot_2025-10-28_at_13.17.54_2x_ip1stl.png)

In the rental configuration form, you can set the minimum and maximum rental days. Click the "Save" button to create the rental configuration.

![Rental configuration form with minimum and maximum rental days fields](https://res.cloudinary.com/dza7lstvk/image/upload/v1761650449/Medusa%20Resources/CleanShot_2025-10-28_at_13.20.02_2x_zvixfc.png)

After saving, you should see the rental configuration details in the widget. You can edit the configuration or toggle its status.

![Rental configuration details in the widget](https://res.cloudinary.com/dza7lstvk/image/upload/v1761650526/Medusa%20Resources/CleanShot_2025-10-28_at_13.21.43_2x_t7ktgq.png)

***

## Step 7: Retrieve Rental Availability API Route

In this step, you'll add an API route that allows customers to check the availability of a product for rental between two dates, and retrieve the total rental price.

### a. Define hasRentalOverlap Method

Before you implement the API route, you'll add a method to the Rental Module's service that checks if a rental overlaps with a given date range.

In `src/modules/rental/service.ts`, add the following method to the `RentalModuleService` class:

```ts title="src/modules/rental/service.ts"
class RentalModuleService extends MedusaService({
  Rental,
  RentalConfiguration,
}) {
  async hasRentalOverlap(variant_id: string, start_date: Date, end_date: Date) {
    const [, count] = await this.listAndCountRentals({
      variant_id,
      status: ["active", "pending"],
      $or: [
        { 
          rental_start_date: { 
            $lte: end_date,
          },
          rental_end_date: {
            $gte: start_date,
          },
        },
      ],
    })

    return count > 0
  }
}
```

The method accepts a product variant ID, a rental start date, and a rental end date.

In the method, you use the `listAndCountRentals` method of the service to count the number of rentals for the given variant that overlap with the provided date range.

If the count is greater than zero, it means there is an overlapping rental, and the method returns `true`. Otherwise, it returns `false`.

### b. Define validateRentalDates Utility

Next, you'll create a utility function to validate rental dates.

Create the file `src/utils/validate-rental-dates.ts` with the following content:

```ts title="src/utils/validate-rental-dates.ts"
import { MedusaError } from "@medusajs/framework/utils"

export default function validateRentalDates(
  rentalStartDate: string | Date,
  rentalEndDate: string | Date,
  rentalConfiguration: {
    min_rental_days: number
    max_rental_days: number | null
  },
  rentalDays: number | string
) {
  const startDate = rentalStartDate instanceof Date ? rentalStartDate : new Date(rentalStartDate)
  const endDate = rentalEndDate instanceof Date ? rentalEndDate : new Date(rentalEndDate)
  const days = typeof rentalDays === "number" ? rentalDays : Number(rentalDays)

  // Validate rental period meets configuration requirements
  if (days < rentalConfiguration.min_rental_days) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      `Rental period of ${days} days is less than the minimum of ${rentalConfiguration.min_rental_days} days`
    )
  }

  if (
    rentalConfiguration.max_rental_days !== null &&
    days > rentalConfiguration.max_rental_days
  ) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      `Rental period of ${days} days exceeds the maximum of ${rentalConfiguration.max_rental_days} days`
    )
  }

  // validate that the dates aren't in the past
  const now = new Date()
  now.setHours(0, 0, 0, 0) // Reset to start of day
  if (startDate < now || endDate < now) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      `Rental dates cannot be in the past. Received start date: ${startDate.toISOString()}, end date: ${endDate.toISOString()}`
    )
  }

  if (endDate <= startDate) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      `rentalEndDate must be after rentalStartDate`
    )
  }
}
```

The function accepts the rental start and end dates, the rental configuration, and the number of rental days.

In the function, you validate:

1. That the rental period meets the minimum and maximum rental days defined in the configuration.
2. That the rental dates are not in the past.
3. That the end date is after the start date.

If any validation fails, you throw a `MedusaError` with the `INVALID_DATA` type.

You'll use this utility function in your customizations.

### c. Rental Availability API Route

Next, you'll create the API route to retrieve the rental availability of a product.

To create the API route, create the file `src/api/store/products/[id]/rental-availability/route.ts` with the following content:

```ts title="src/api/store/products/[id]/rental-availability/route.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { MedusaError, QueryContext } from "@medusajs/framework/utils"
import { z } from "zod"
import { RENTAL_MODULE } from "../../../../../modules/rental"
import RentalModuleService from "../../../../../modules/rental/service"
import validateRentalDates from "../../../../../utils/validate-rental-dates"

export const GetRentalAvailabilitySchema = z.object({
  variant_id: z.string(),
  start_date: z.string().refine((val) => !isNaN(Date.parse(val)), {
    message: "start_date must be a valid date string (YYYY-MM-DD)",
  }),
  end_date: z
    .string()
    .optional()
    .refine((val) => val === undefined || !isNaN(Date.parse(val)), {
      message: "end_date must be a valid date string (YYYY-MM-DD)",
    }),
  currency_code: z.string().optional(),
})

export const GET = async (
  req: MedusaRequest<{}, z.infer<typeof GetRentalAvailabilitySchema>>, 
  res: MedusaResponse
) => {
  const { id: productId } = req.params
 
  const { 
    variant_id, 
    start_date, 
    end_date,
    currency_code,
  } = req.validatedQuery

  const query = req.scope.resolve("query")
  const rentalModuleService: RentalModuleService = req.scope.resolve(
    RENTAL_MODULE
  )

  // Parse dates
  const rentalStartDate = new Date(start_date)
  const rentalEndDate = end_date ? new Date(end_date) : new Date(rentalStartDate)
  
  // If no end_date provided, assume single day rental (same day)
  if (!end_date) {
    rentalEndDate.setHours(23, 59, 59, 999)
  }

  // TODO retrieve and validate rental configuration
}
```

You define a Zod schema to validate the query parameters of the request. You also expose a `GET` API route at `/store/products/:id/rental-availability`.

In the route handler, you parse the start and end dates.

Next, you'll implement the logic to retrieve and validate the rental configuration. Replace the `// TODO retrieve and validate rental configuration` comment with the following code:

```ts title="src/api/store/products/[id]/rental-availability/route.ts"
const { data: [rentalConfig] } = await query.graph({
  entity: "rental_configuration",
  fields: ["*"],
  filters: { 
    product_id: productId,
    status: "active",
  },
})

if (!rentalConfig) {
  throw new MedusaError(
    MedusaError.Types.NOT_FOUND,
    "product is not rentable"
  )
}

const rentalDays = Math.ceil(
  (rentalEndDate.getTime() - rentalStartDate.getTime()) / 
  (1000 * 60 * 60 * 24)
) + 1 // +1 to include both start and end date

validateRentalDates(
  rentalStartDate, 
  rentalEndDate, 
  {
    min_rental_days: rentalConfig.min_rental_days,
    max_rental_days: rentalConfig.max_rental_days,
  }, 
  rentalDays
)

// TODO check for overlapping rentals and calculate price
```

You retrieve the active rental configuration for the product using Query. Then, you calculate the rental period in days, and you validate the rental dates using the `validateRentalDates` utility you created earlier.

Next, you'll implement the logic to check for overlapping rentals and calculate the rental price. Replace the `// TODO check for overlapping rentals and calculate price` comment with the following code:

```ts title="src/api/store/products/[id]/rental-availability/route.ts"
// Check if variant is already rented during the requested period
const isAvailable = !await rentalModuleService.hasRentalOverlap(
  variant_id, 
  rentalStartDate, 
  rentalEndDate
)
let price = 0
if (isAvailable && currency_code) {
  const { data: [variant] } = await query.graph({
    entity: "product_variant",
    fields: ["calculated_price.*"],
    filters: {
      id: variant_id,
    },
    context: {
      calculated_price: QueryContext({
        currency_code: currency_code,
      }),
    },
  })
  price = ((variant as any).calculated_price?.calculated_amount || 0) *
    rentalDays
}

res.json({
  available: isAvailable,
  price: {
    amount: price,
    currency_code: currency_code,
  },
})
```

You use the `hasRentalOverlap` method you defined earlier to check if there are any overlapping rentals for the specified variant and date range.

If the variant is available and a currency code is provided, you retrieve the variant's calculated price using Query and calculate the total rental price based on the number of rental days.

Finally, you return the availability status and the total rental price in the response.

### c. Add Query Validation Middleware

To validate the query parameters of requests sent to the Rental Availability API route, you'll apply a middleware.

In `src/api/middlewares.ts`, add the following imports at the top of the file:

```ts title="src/api/middlewares.ts"
import { 
  validateAndTransformQuery,
} from "@medusajs/framework/http"
import { 
  GetRentalAvailabilitySchema,
} from "./store/products/[id]/rental-availability/route"
```

Then, pass a new object to the `routes` array in `defineMiddlewares`:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/store/products/:id/rental-availability",
      methods: ["GET"],
      middlewares: [
        validateAndTransformQuery(GetRentalAvailabilitySchema, {}),
      ],
    },
  ],
})
```

You apply the `validateAndTransformQuery` middleware to the `GET` route of the `/store/products/:id/rental-availability` path, passing it the Zod schema you created in the route file.

You'll use this API route in the next step to check the rental availability of products.

***

## Step 8: Show Rental Options in Storefront

In this step, you'll customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to show rental options on the product details page, allowing customers to choose rental dates when the product is rentable.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-product-rental`, you can find the storefront by going back to the parent directory and changing to the `medusa-product-rental-storefront` directory:

```bash
cd ../medusa-product-rental-storefront # change based on your project name
```

### a. Define Types

First, you'll define types for the rental configuration and rental availability response.

Create the file `src/types/rental.ts` in the storefront directory with the following content:

```ts title="src/types/rental.ts" badgeLabel="Storefront" badgeColor="blue"
export interface RentalConfiguration {
  min_rental_days: number
  max_rental_days: number | null
  status: "active" | "inactive"
}

export interface RentalAvailabilityResponse {
  available: boolean
  message?: string
  price?: {
    amount: number
    currency_code: string | null
  }
}
```

You'll use these types in the next sections.

### b. Fetch Rental Configuration with Product Details

Next, you'll ensure that the rental configuration is fetched when retrieving the product details. You can retrieve linked data models by passing its name in the `fields` query parameter when fetching the product.

In `src/lib/data/products.ts`, find the `listProducts` function and update the `fields` parameter passed to the JS SDK function call to include the `*rental_configuration` field:

```ts title="src/lib/data/products.ts" badgeLabel="Storefront" badgeColor="blue" highlights={[["16"]]}
export const listProducts = async ({
  // ...
}: {
  // ...
}): Promise<{
  // ...
}> => {
  // ...
  return sdk.client
    .fetch<{ products: HttpTypes.StoreProduct[]; count: number }>(
      `/store/products`,
      {
        query: {
          // ...
          fields:
            "*variants.calculated_price,+variants.inventory_quantity,+metadata,+tags,*rental_configuration",
        },
        // ...
      }
    )
  // ...
}
```

You pass `*rental_configuration` at the end of the `fields` parameter. This will attach a `rental_configuration` object to each product returned by the API if it has one.

### c. Add Rental Availability Function

Next, you'll add a server function that retrieves the rental availability of a product by calling the Rental Availability API route you created earlier.

Create the file `src/lib/data/rentals.ts` with the following content:

```ts title="src/lib/data/rentals.ts" badgeLabel="Storefront" badgeColor="blue"
"use server"

import { sdk } from "@lib/config"
import { getAuthHeaders, getCacheOptions } from "./cookies"
import { RentalAvailabilityResponse } from "../../types/rental"

export const getRentalAvailability = async ({
  productId,
  variantId,
  startDate,
  endDate,
  currencyCode,
}: {
  productId: string
  variantId: string
  startDate: string
  endDate?: string
  currencyCode?: string
}): Promise<RentalAvailabilityResponse> => {
  const headers = {
    ...(await getAuthHeaders()),
  }

  const next = {
    ...(await getCacheOptions("rental-availability")),
  }

  const queryParams: Record<string, any> = {
    variant_id: variantId,
    start_date: startDate,
  }

  if (endDate) {
    queryParams.end_date = endDate
  }

  if (currencyCode) {
    queryParams.currency_code = currencyCode
  }

  return sdk.client
    .fetch<RentalAvailabilityResponse>(
      `/store/products/${productId}/rental-availability`,
      {
        method: "GET",
        query: queryParams,
        headers,
        next,
        cache: "no-store", // Always fetch fresh data for availability
      }
    )
    .then((data) => data)
}
```

The `getRentalAvailability` function accepts the product ID, variant ID, rental start date, optional rental end date, and optional currency code.

In the function, you send a `GET` request to the Rental Availability API route, passing the parameters as query parameters.

The function returns the rental availability response.

### d. Create Rental Date Picker Component

Next, you'll create the component that shows start and end date pickers for selecting rental dates. You'll show this component for rentable products only.

Create the file `src/modules/products/components/rental-date-picker/index.tsx` with the following content:

```tsx title="src/modules/products/components/rental-date-picker/index.tsx" badgeLabel="Storefront" badgeColor="blue" collapsibleLines="1-8" expandButtonLabel="Show Imports"
"use client"

import { useState, useCallback, useMemo } from "react"
import { DatePicker } from "@medusajs/ui"
import { HttpTypes } from "@medusajs/types"
import { getRentalAvailability } from "@lib/data/rentals"
import { RentalConfiguration } from "../../../../types/rental"

type RentalDatePickerProps = {
  product: HttpTypes.StoreProduct
  selectedVariant?: HttpTypes.StoreProductVariant
  region: HttpTypes.StoreRegion
  onDatesSelected: (data: {
    startDate: string
    endDate: string
    days: number
    price?: { amount: number; currency_code: string | null }
  }) => void
  disabled?: boolean
}

export default function RentalDatePicker({
  product,
  selectedVariant,
  region,
  onDatesSelected,
  disabled = false,
}: RentalDatePickerProps) {
  const [startDate, setStartDate] = useState<Date | null>(null)
  const [endDate, setEndDate] = useState<Date | null>(null)
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const rentalConfig = useMemo(() => {
    return "rental_configuration" in product
      ? (product.rental_configuration as RentalConfiguration | undefined)
      : undefined
  }, [product])

  // TODO define functions
}
```

You define the `RentalDatePicker` component that accepts the following props:

1. `product`: The product object.
2. `selectedVariant`: The product variant that the customer has selected.
3. `region`: The region that the customer is viewing the product in.
4. `onDatesSelected`: A callback function that is called when the customer selects valid rental dates.
5. `disabled`: An optional boolean to disable the date picker.

In the component, you define state variables to manage the selected start and end dates, loading state, and error messages. You also create a memoized variable for the rental configuration.

Next, you'll add the functions to handle date selection and availability checking. Replace the `// TODO define functions` comment with the following code:

```tsx title="src/modules/products/components/rental-date-picker/index.tsx" badgeLabel="Storefront" badgeColor="blue"
// Memoized rental days calculation for display
const rentalDays = useCallback((start: Date, end: Date) => {
  if (!start || !end) {return 0}
  return Math.ceil(
    (end.getTime() - start.getTime()) / (1000 * 3600 * 24)
  ) + 1 // +1 to include both start and end dates
}, [])

// Helper function to check if date is in the past
const isDateInPast = (date: Date) => {
  const today = new Date()
  today.setHours(0, 0, 0, 0)
  return date < today ? "Date cannot be in the past" : true
}

// Helper function to format date to YYYY-MM-DD string
const formatDateToString = (date: Date): string => {
  const year = date.getFullYear()
  const month = String(date.getMonth() + 1).padStart(2, "0")
  const day = String(date.getDate()).padStart(2, "0")
  return `${year}-${month}-${day}`
}

// Memoized comprehensive validation and availability checking
const validateAndCheckAvailability = useCallback(async (start: Date, end: Date) => {
  if (!selectedVariant?.id || !rentalConfig) {
    return
  }
  setError(null)

  try {
    const startDateString = formatDateToString(start)
    const endDateString = formatDateToString(end)

    // 1. Validate date order (allow same day for single day rental)
    if (end < start) {
      setError("End date cannot be before start date")
      return
    }

    const days = rentalDays(start, end)
    
    if (rentalConfig.min_rental_days && days < rentalConfig.min_rental_days) {
      setError(`Minimum rental period is ${rentalConfig.min_rental_days} days`)
      return
    }
    
    if (rentalConfig.max_rental_days && days > rentalConfig.max_rental_days) {
      setError(`Maximum rental period is ${rentalConfig.max_rental_days} days`)
      return
    }

    setIsLoading(true)

    // 3. Check availability with backend
    const availability = await getRentalAvailability({
      productId: product.id,
      variantId: selectedVariant.id,
      startDate: startDateString,
      endDate: endDateString,
      currencyCode: region.currency_code,
    })

    if (!availability.available) {
      setError(availability.message || "Selected rental period is not available")
      return
    }

    // 4. If everything is valid, call the callback with price information
    setError(null)
    onDatesSelected({
      startDate: startDateString,
      endDate: endDateString,
      days: days,
      price: availability.price,
    })

  } catch (err) {
    setError("Failed to check rental availability")
    console.error("Rental availability error:", err)
  } finally {
    setIsLoading(false)
  }
}, [selectedVariant?.id, rentalConfig, product.id, onDatesSelected, rentalDays])

// Memoized date change handlers to prevent recreation on every render
const handleStartDateChange = useCallback((date: Date | null) => {
  setStartDate(date)
  setError(null)
  // Trigger comprehensive validation if both dates are now selected
  if (date && endDate) {
    validateAndCheckAvailability(date, endDate)
  }
}, [endDate, validateAndCheckAvailability])

const handleEndDateChange = useCallback((date: Date | null) => {
  setEndDate(date)
  setError(null)
  // Trigger comprehensive validation if both dates are now selected
  if (date && startDate) {
    validateAndCheckAvailability(startDate, date)
  }
}, [startDate, validateAndCheckAvailability])

// TODO render component
```

You define the following functions:

- `rentalDays`: Calculates the number of rental days between two dates.
- `isDateInPast`: Checks if a given date is in the past.
- `formatDateToString`: Formats a date to a `YYYY-MM-DD` string.
- `validateAndCheckAvailability`: A comprehensive function that validates the selected dates against the rental configuration and checks availability with the backend.
- `handleStartDateChange` and `handleEndDateChange`: Handlers for when the start and end dates are changed. They call the `validateAndCheckAvailability` function if both dates are selected.

Finally, you'll add the `return` statement to render the component's UI. Replace the `// TODO render component` comment with the following code:

```tsx title="src/modules/products/components/rental-date-picker/index.tsx" badgeLabel="Storefront" badgeColor="blue"
if (rentalConfig?.status !== "active") {
  return null
}

return (
  <div className="space-y-4">
    <div className="text-sm font-medium">Rental Period</div>
    
    <div className="flex flex-col gap-4">
      <div>
        <label className="block text-sm font-medium mb-2">From</label>
        <DatePicker
          value={startDate}
          onChange={handleStartDateChange}
          isDisabled={disabled || isLoading}
          minValue={new Date()}
          validate={(date) => {
            return isDateInPast(new Date(date.toString()))
          }}
        />
      </div>
      
      <div>
        <label className="block text-sm font-medium mb-2">Until</label>
        <DatePicker
          value={endDate}
          onChange={handleEndDateChange}
          isDisabled={disabled || isLoading || !startDate}
          minValue={startDate || new Date()}
          validate={(date) => {
            return isDateInPast(new Date(date.toString()))
          }}
        />
      </div>
    </div>

    {error && (
      <div className="text-sm text-red-600">
        {error}
      </div>
    )}

    {isLoading && (
      <div className="text-sm text-gray-500">
        Checking availability...
      </div>
    )}

    {startDate && endDate && !error && !isLoading && (
      <div className="text-sm text-ui-fg-subtle">
        Rental period: {rentalDays(startDate, endDate)} days
      </div>
    )}
  </div>
)
```

If the product does not have an active rental configuration, you return `null` to avoid rendering anything.

Otherwise, you render two date pickers for selecting the rental start and end dates. You also show error messages, loading indicators, and the calculated rental period.

### e. Customize Product Price Component

Next, you'll customize the product price component to show the rental price for rental products.

Replace the file content in `src/modules/products/components/product-price/index.tsx` with the following code:

```tsx title="src/modules/products/components/product-price/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { clx } from "@medusajs/ui"

import { getProductPrice } from "@lib/util/get-product-price"
import { HttpTypes } from "@medusajs/types"
import { convertToLocale } from "../../../../lib/util/money"
import { RentalAvailabilityResponse } from "../../../../types/rental"

export default function ProductPrice({
  product,
  variant,
  rentalPrice,
  is_rental = false,
}: {
  product: HttpTypes.StoreProduct
  variant?: HttpTypes.StoreProductVariant
  rentalPrice?: RentalAvailabilityResponse["price"] | null
  is_rental?: boolean
}) {
  const { cheapestPrice, variantPrice } = getProductPrice({
    product,
    variantId: variant?.id,
  })

  const selectedPrice = variant ? variantPrice : cheapestPrice

  // Use rental price if available, otherwise use regular price
  const displayPrice = rentalPrice ? {
    calculated_price: convertToLocale({
      amount: rentalPrice.amount,
      currency_code: rentalPrice.currency_code!,
    }),
    calculated_price_number: rentalPrice.amount,
    price_type: "default" as const,
    original_price: "",
    original_price_number: 0,
    percentage_diff: "",
  } : selectedPrice

  if (!displayPrice) {
    return <div className="block w-32 h-9 bg-gray-100 animate-pulse" />
  }

  return (
    <div className="flex flex-col text-ui-fg-base">
      <span
        className={clx("text-xl-semi", {
          "text-ui-fg-interactive": displayPrice.price_type === "sale",
        })}
      >
        {!variant && !rentalPrice && "From "}
        <span
          data-testid="product-price"
          data-value={displayPrice.calculated_price_number}
        >
          {displayPrice.calculated_price}
        </span>
        {!rentalPrice && is_rental && <span className="text-xs text-ui-fg-muted ml-1">per day</span>}
      </span>
      {displayPrice.price_type === "sale" && (
        <>
          <p>
            <span className="text-ui-fg-subtle">Original: </span>
            <span
              className="line-through"
              data-testid="original-product-price"
              data-value={displayPrice.original_price_number}
            >
              {displayPrice.original_price}
            </span>
          </p>
          <span className="text-ui-fg-interactive">
            -{displayPrice.percentage_diff}%
          </span>
        </>
      )}
    </div>
  )
}
```

You make the following key changes:

1. Add a new optional prop `rentalPrice` to accept the rental price information.
2. Add a new optional prop `is_rental` that indicates if the product is a rentable product.
3. Add a `displayPrice` variable that uses the rental price if available; otherwise, it falls back to the regular product price.
4. Update the price display to show "per day" if the product is rentable and no rental price is provided.

For non-rentable products, the price is shown as usual.

### f. Show Rental Options on Product Details Page

Finally, you'll customize the product actions component shown on the product details page to display the rental date picker and pass the rental price to the product price component.

In `src/modules/products/components/product-actions/index.tsx`, add the following imports at the top of the file:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import RentalDatePicker from "../rental-date-picker"
import { 
  RentalAvailabilityResponse, 
  RentalConfiguration,
} from "../../../../types/rental"
```

Then, in the `ProductActions` component, destructure the `region` prop:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
export default function ProductActions({
  // ...
  region,
}: ProductActionsProps) {
  // ...
}
```

Next, add the state variables in the `ProductActions` component:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const [rentalStartDate, setRentalStartDate] = useState<string | null>(null)
const [rentalEndDate, setRentalEndDate] = useState<string | null>(null)
const [rentalDays, setRentalDays] = useState<number | null>(null)
const [rentalPrice, setRentalPrice] = useState<RentalAvailabilityResponse["price"] | null>(null)

const rentalConfig = "rental_configuration" in product ?
  product.rental_configuration as RentalConfiguration | undefined : undefined
const isRentable = rentalConfig?.status === "active"

// Check if rental dates are required and selected
const rentalDatesValid = useMemo(() => {
  return !isRentable || (!!rentalStartDate && !!rentalEndDate && !!rentalDays)
}, [isRentable, rentalStartDate, rentalEndDate, rentalDays])
```

You define the following variables:

- `rentalStartDate` and `rentalEndDate`: To store the selected rental dates.
- `rentalDays`: To store the number of rental days.
- `rentalPrice`: To store the rental price information.
- `rentalConfig`: Holds the rental configuration of the product.
- `isRentable`: A boolean indicating if the product is rentable.
- `rentalDatesValid`: A memoized value that checks if rental dates are required and have been selected.

Next, add to the component a function that handles when rental dates are selected:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const handleRentalDatesSelected = (data: {
  startDate: string
  endDate: string
  days: number
  price?: { amount: number; currency_code: string | null }
}) => {
  setRentalStartDate(data.startDate)
  setRentalEndDate(data.endDate)
  setRentalDays(data.days)
  setRentalPrice(data.price || null)
}
```

This function sets the state variables when rental dates are selected.

Then, in the `return` statement of the `ProductActions` component, add the following before the `ProductPrice` component:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
{isRentable && (
  <>
    <RentalDatePicker
      product={product}
      selectedVariant={selectedVariant}
      region={region}
      onDatesSelected={handleRentalDatesSelected}
      disabled={!!disabled || isAdding}
    />

    <Divider className="my-4" />
  </>
)}
```

And update the `ProductPrice` component to pass the `rentalPrice` and `is_rental` props:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
<ProductPrice 
  product={product} 
  variant={selectedVariant} 
  rentalPrice={rentalPrice} 
  is_rental={isRentable} 
/>
```

Finally, update the "Add to Cart" button to be disabled if rental dates are required but not selected:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
<Button
  onClick={handleAddToCart}
  disabled={
    !inStock ||
    !selectedVariant ||
    !!disabled ||
    isAdding ||
    !isValidVariant ||
    !rentalDatesValid
  }
  variant="primary"
  className="w-full h-10"
  isLoading={isAdding}
  data-testid="add-product-button"
>
  {!selectedVariant && !options
    ? "Select variant"
    : !inStock || !isValidVariant
    ? "Out of stock"
    : !rentalDatesValid
    ? "Select rental dates"
    : "Add to cart"}
</Button>
```

### g. Test Rental Options in Storefront

You can now view and select the rental options in the Next.js Starter Storefront.

First, run the following command in the Medusa application's directory to start the Medusa server:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

Then, in a separate terminal, navigate to the Next.js Starter Storefront directory and run the following command to start the storefront:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

Open the storefront in `http://localhost:8000` and go to Menu -> Store. Click on a rentable product to view its details.

On the right side, you'll find the rental date picker component where you can select the rental start and end dates. This will update the rental price shown above the "Add to Cart" button.

You haven't implemented the add-to-cart functionality for rentable products yet. You'll do that in the next step.

![Rental options on product details page in the storefront](https://res.cloudinary.com/dza7lstvk/image/upload/v1761653559/Medusa%20Resources/CleanShot_2025-10-28_at_14.12.25_2x_g09h85.png)

***

## Step 9: Add Rental Products to Cart

In this step, you'll implement the logic to add rentable products to the cart in the Medusa application. You'll wrap Medusa's existing add-to-cart logic to include rental-specific data and validation.

You'll create a workflow with the logic to add rental products to the cart, and an API route that uses this workflow.

### a. Add Products with Rental to Cart Workflow

First, you'll create a workflow that contains the logic to add products to the cart, with support for rental products.

The workflow will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve cart details.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve details of the variant to add to the cart.
- [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md): Acquire a lock on the cart to prevent concurrent modifications.
- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md): Add the product to the cart.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve updated cart details.
- [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md): Release the lock on the cart.

Medusa provides all the steps and workflows out-of-the-box, except for the `validateRentalCartItemStep` step, which you'll implement.

#### hasCartOverlap Utility

Before you implement the workflow and its steps, you'll create a utility function that checks if an item overlaps with existing rental items in the cart.

Create the file `src/utils/has-cart-overlap.ts` with the following content:

```ts title="src/utils/has-cart-overlap.ts" badgeLabel="Medusa Application" badgeColor="green"
export default function hasCartOverlap(
  item: {
    variant_id: string
    rental_start_date: Date
    rental_end_date: Date
    rental_days: number
  },
  cart_items: {
    id: string
    variant_id: string
    metadata?: Record<string, unknown>
  }[]
): boolean {
  for (const cartItem of cart_items) {
    if (cartItem.variant_id !== item.variant_id) {
      continue
    }

    // Check if this cart item is also a rental with metadata
    const cartItemMetadata = cartItem.metadata || {}
    const existingStartStr = cartItemMetadata.rental_start_date
    const existingEndStr = cartItemMetadata.rental_end_date
    const existingDays = cartItemMetadata.rental_days

    if (!existingStartStr || !existingEndStr || !existingDays) {
      continue
    }

    // Both are rental items, check for date overlap
    const existingStartDate = new Date(existingStartStr as string)
    const existingEndDate = new Date(existingEndStr as string)

    // Check if dates overlap
    const hasOverlap = item.rental_start_date <= existingEndDate && item.rental_end_date >= existingStartDate

    if (hasOverlap) {return true}
  }

  return false
}
```

The `hasCartOverlap` function accepts a rental item and a list of existing cart items.

In the function, you loop through the existing cart items and check if any of them are rental items for the same variant with an overlapping rental period.

The function returns `true` if an overlap is found, otherwise it returns `false`.

#### validateRentalCartItemStep

The `validateRentalCartItemStep` validates the rental data provided and retrieves the rental days and price.

To create the step, create the file `src/workflows/steps/validate-rental-cart-item.ts` with the following content:

```ts title="src/workflows/steps/validate-rental-cart-item.ts" badgeLabel="Medusa Application" badgeColor="green" collapsibleLines="1-8" expandButtonLabel="Show Imports"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { MedusaError } from "@medusajs/framework/utils"
import { RENTAL_MODULE } from "../../modules/rental"
import RentalModuleService from "../../modules/rental/service"
import { InferTypeOf, ProductVariantDTO } from "@medusajs/framework/types"
import { RentalConfiguration } from "../../modules/rental/models/rental-configuration"
import hasCartOverlap from "../../utils/has-cart-overlap"
import validateRentalDates from "../../utils/validate-rental-dates"

export type ValidateRentalCartItemInput = {
  variant: ProductVariantDTO
  quantity: number
  metadata?: Record<string, unknown>
  rental_configuration: InferTypeOf<typeof RentalConfiguration> | null
  existing_cart_items: {
    id: string
    variant_id: string
    metadata?: Record<string, unknown>
  }[]
}

export const validateRentalCartItemStep = createStep(
  "validate-rental-cart-item",
  async ({ 
    variant, 
    quantity, 
    metadata, 
    rental_configuration, 
    existing_cart_items,
  }: ValidateRentalCartItemInput, { container }) => {
    const rentalModuleService: RentalModuleService = container.resolve(RENTAL_MODULE)

    // Skip validation if not a rental product or if rental config is not active
    if (rental_configuration?.status !== "active") {
      return new StepResponse({ is_rental: false, rental_days: 0, price: 0 })
    }

    // This is a rental product - validate quantity
    if (quantity !== 1) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        `Rental items must have a quantity of 1. Cannot add ${quantity} of variant ${variant.id}`
      )
    }

    // TODO validate metadata
  }
)
```

The `validateRentalCartItemStep` accepts the following props:

- `variant`: The product variant to add to the cart.
- `quantity`: The quantity of the variant to add.
- `metadata`: Optional metadata associated with the cart item. This metadata should include rental options like start and end dates.
- `rental_configuration`: The rental configuration of the product variant.
- `existing_cart_items`: The existing items in the cart.

In the step, you first return early if the product is not a rental product or if the rental configuration is not active. You also validate that the quantity is `1`, as rental items must have a quantity of one.

Next, you'll validate that the necessary rental options are provided in the item's metadata. Replace the `// TODO validate metadata` comment with the following code:

```ts title="src/workflows/steps/validate-rental-cart-item.ts" badgeLabel="Medusa Application" badgeColor="green"
// Validate metadata
const rentalStartDate = metadata?.rental_start_date
const rentalEndDate = metadata?.rental_end_date
const rentalDays = metadata?.rental_days

if (!rentalStartDate || !rentalEndDate || !rentalDays) {
  throw new MedusaError(
    MedusaError.Types.INVALID_DATA,
    `Rental product variant ${variant.id} requires rental_start_date, rental_end_date and rental_days in metadata`
  )
}

const startDate = new Date(rentalStartDate as string)
const endDate = new Date(rentalEndDate as string)
const days = typeof rentalDays === "number" ? rentalDays : Number(rentalDays)

validateRentalDates(
  startDate, 
  endDate, 
  {
    min_rental_days: rental_configuration.min_rental_days,
    max_rental_days: rental_configuration.max_rental_days,
  }, 
  days
)

// TODO validate that there's no overlap with cart items or existing rentals
```

You validate that the `rental_start_date`, `rental_end_date`, and `rental_days` are provided in the metadata. These are necessary to process the rental and will be stored in the line item's `metadata` property.

You also validate the rental dates using the `validateRentalDates` utility function you created earlier.

Next, you'll validate that the rental period does not overlap with existing rentals in the cart or existing rentals for the same variant. Replace the `// TODO validate that there's no overlap with cart items or existing rentals` comment with the following code:

```ts title="src/workflows/steps/validate-rental-cart-item.ts" badgeLabel="Medusa Application" badgeColor="green"
// Check if this rental variant is already in the cart with overlapping dates
const hasCartOverlapResult = hasCartOverlap(
  {
    variant_id: variant.id,
    rental_start_date: startDate,
    rental_end_date: endDate,
    rental_days: days,
  },
  existing_cart_items
)

if (hasCartOverlapResult) {
  throw new MedusaError(
    MedusaError.Types.INVALID_DATA,
    `Rental variant ${variant.id} is already in the cart with overlapping dates (${startDate.toISOString().split("T")[0]} to ${endDate.toISOString().split("T")[0]})`
  )
}

// Check availability for the requested period
const hasOverlap = await rentalModuleService.hasRentalOverlap(variant.id, startDate, endDate)

if (hasOverlap) {
  throw new MedusaError(
    MedusaError.Types.NOT_ALLOWED,
    `Variant ${variant.id} is already rented during the requested period (${startDate.toISOString()} to ${endDate.toISOString()})`
  )
}

return new StepResponse({ 
  is_rental: true,
  rental_days: days,
  price: ((variant as any).calculated_price?.calculated_amount || 0) * days,
})
```

You first check if the rental start and end dates are in the past, and if the end date is after the start date.

Then, you check for overlaps with existing cart items using the `hasCartOverlap` utility you created earlier. If there are overlaps, you throw an error.

Next, you use the `hasRentalOverlap` method from the Rental Module's service to check if there are any overlapping rentals for the specified variant and date range. If there are overlaps, you throw an error.

Finally, you return a `StepResponse` indicating that the item is a rental, along with the number of rental days and the total price for the rental period.

#### Add Products with Rental to Cart Workflow

You can now create the `addToCartWithRentalWorkflow` that uses the `validateRentalCartItemStep` step.

Create the file `src/workflows/add-to-cart-with-rental.ts` with the following content:

```ts title="src/workflows/add-to-cart-with-rental.ts" badgeLabel="Medusa Application" badgeColor="green" collapsibleLines="1-18" expandButtonLabel="Show Imports"
import { 
  createWorkflow, 
  WorkflowResponse, 
  transform,
  when, 
} from "@medusajs/framework/workflows-sdk"
import { 
  acquireLockStep, 
  addToCartWorkflow, 
  releaseLockStep, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { QueryContext } from "@medusajs/framework/utils"
import { 
  ValidateRentalCartItemInput, 
  validateRentalCartItemStep,
} from "./steps/validate-rental-cart-item"

type AddToCartWorkflowInput = {
  cart_id: string
  variant_id: string
  quantity: number
  metadata?: Record<string, unknown>
}

export const addToCartWithRentalWorkflow = createWorkflow(
  "add-to-cart-with-rental",
  (input: AddToCartWorkflowInput) => {
    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      fields: ["id", "currency_code", "region_id", "items.*"],
      filters: { id: input.cart_id },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const { data: variants } = useQueryGraphStep({
      entity: "product_variant",
      fields: [
        "id",
        "product.id",
        "product.rental_configuration.*",
        "calculated_price.*",
      ],
      filters: {
        id: input.variant_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
      context: {
        calculated_price: QueryContext({
          currency_code: carts[0].currency_code,
          region_id: carts[0].region_id,
        }),
      },
    }).config({ name: "retrieve-variant" })

    const rentalData = when({ variants }, (data) => {
      return data.variants[0].product?.rental_configuration?.status === "active"
    }).then(() => {
      return validateRentalCartItemStep({
        variant: variants[0],
        quantity: input.quantity,
        metadata: input.metadata,
        rental_configuration: variants[0].product?.rental_configuration || null,
        existing_cart_items: carts[0].items,
      } as unknown as ValidateRentalCartItemInput)
    })

    acquireLockStep({
      key: input.cart_id,
      timeout: 2,
      ttl: 10,
    })

    const itemToAdd = transform({
      input,
      rentalData,
      variants,
    }, (data) => {
      const baseItem = {
        variant_id: data.input.variant_id,
        quantity: data.input.quantity,
        metadata: data.input.metadata,
      }

      // If it's a rental product, use the calculated rental price
      if (data.rentalData?.is_rental && data.rentalData.price) {
        return [{
          ...baseItem,
          unit_price: data.rentalData.price,
        }]
      }

      // For non-rental products, don't specify unit_price (let Medusa calculate it)
      return [baseItem]
    })

    addToCartWorkflow.runAsStep({
      input: {
        cart_id: input.cart_id,
        items: itemToAdd as any,
      },
    })

    const { data: updatedCart } = useQueryGraphStep({
      entity: "cart",
      fields: ["*", "items.*"],
      filters: {
        id: input.cart_id,
      },
    }).config({ name: "refetch-cart" })

    releaseLockStep({
      key: input.cart_id,
    })

    return new WorkflowResponse({
      cart: updatedCart[0],
    })
  }
)
```

You create the `addToCartWithRentalWorkflow` workflow that accepts the cart ID, variant ID, quantity, and optional metadata.

In the workflow, you:

1. Retrieve the cart details using the `useQueryGraphStep`.
2. Retrieve the product variant details using the `useQueryGraphStep`.
3. If the product is rentable, call the `validateRentalCartItemStep` to validate and retrieve rental data.
4. Acquire a lock on the cart using the `acquireLockStep`.
5. Prepare the item to add to the cart.
   - If it's a rentable product, you set the `unit_price` to the calculated rental price.
   - For non-rentable products, you don't specify the `unit_price`; Medusa will use the variant's price.
6. Add the item to the cart using the existing `addToCartWorkflow`.
7. Retrieve the updated cart details.
8. Release the lock on the cart using the `releaseLockStep`.

Finally, you return the updated cart in the workflow response.

### b. Add to Cart with Rental API Route

Next, you'll create an API route that uses the `addToCartWithRentalWorkflow` to add products to the cart, including rental products.

Create the file `src/api/store/carts/[id]/line-items/rentals/route.ts` with the following content:

```ts title="src/api/store/carts/[id]/line-items/rentals/route.ts" badgeLabel="Medusa Application" badgeColor="green" collapsibleLines="1-9" expandButtonLabel="Show Imports"
import type { 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { 
  addToCartWithRentalWorkflow,
} from "../../../../../../workflows/add-to-cart-with-rental"
import { z } from "zod"

export const PostCartItemsRentalsBody = z.object({
  variant_id: z.string(),
  quantity: z.number(),
  metadata: z.record(z.string(), z.unknown()).optional(),
})

export const POST = async (
  req: MedusaRequest<z.infer<typeof PostCartItemsRentalsBody>>,
  res: MedusaResponse
) => {
  const { id: cart_id } = req.params
  const { variant_id, quantity, metadata } = req.validatedBody

  const { result } = await addToCartWithRentalWorkflow(req.scope).run({
    input: {
      cart_id,
      variant_id,
      quantity,
      metadata,
    },
  })

  res.json({ cart: result.cart })
}
```

You create a Zod schema to validate the request body, which includes the `variant_id`, `quantity`, and optional `metadata`.

You expose a `POST` API route at `/store/carts/{id}/line-items/rentals`. In the route handler, you execute the `addToCartWithRentalWorkflow` passing it the necessary input.

You return the updated cart in the response.

### c. Add Validation Middleware

Next, you'll add validation middleware to ensure that the request body for adding rental items to the cart is valid.

In `src/api/middlewares.ts`, add the following import at the top of the file:

```ts title="src/api/middlewares.ts" badgeLabel="Medusa Application" badgeColor="green"
import { PostCartItemsRentalsBody } from "./store/carts/[id]/line-items/rentals/route"
```

Then, pass a new object to the `routes` array in `defineMiddlewares`:

```ts title="src/api/middlewares.ts" badgeLabel="Medusa Application" badgeColor="green"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/store/carts/:id/line-items/rentals",
      methods: ["POST"],
      middlewares: [
        validateAndTransformBody(PostCartItemsRentalsBody),
      ],
    },
  ],
})
```

You apply the `validateAndTransformBody` middleware to the rental add-to-cart route, using the `PostCartItemsRentalsBody` schema to validate incoming requests.

In the next step, you'll customize the storefront to use this new API route when adding rental products to the cart.

***

## Step 10: Add Rental Products to Cart in Storefront

In this step, you'll customize the Next.js Starter Storefront to use the new rental add-to-cart API route when adding products to the cart.

### a. Update Add to Cart Function

First, you'll update the `addToCart` function to use the rental add-to-cart API route when adding rental products to the cart.

In `src/lib/data/cart.ts`, find the `addToCart` function and add a `metadata` property to its object parameter:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
export async function addToCart({
  variantId,
  quantity,
  countryCode,
  metadata,
}: {
  variantId: string
  quantity: number
  countryCode: string
  metadata?: Record<string, any>
}) {
  // ...
}
```

Then, in the function, change the JS SDK call to the following:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
await sdk.client
.fetch(`/store/carts/${cart.id}/line-items/rentals`, {
  method: "POST",
  body: {
    variant_id: variantId,
    quantity,
    metadata,
  },
  headers,
})
// ...
```

You send a `POST` request to `/store/carts/{id}/line-items/rentals`, passing the `variant_id`, `quantity`, and `metadata` in the request body.

### b. Pass Rental Metadata when Adding to Cart

Next, you'll update the product actions component to pass the rental metadata when adding rentable products to the cart.

In `src/modules/products/components/product-actions/index.tsx`, find the `handleAddToCart` function in the `ProductActions` component and update it to the following:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const handleAddToCart = async () => {
  if (!selectedVariant?.id) {return null}

  setIsAdding(true)

  await addToCart({
    variantId: selectedVariant.id,
    quantity: 1,
    countryCode,
    metadata: isRentable ? {
      rental_start_date: rentalStartDate,
      rental_end_date: rentalEndDate,
      rental_days: rentalDays,
    } : undefined,
  })

  setIsAdding(false)
}
```

If the product is rentable, you pass the `rental_start_date`, `rental_end_date`, and `rental_days` in the `metadata` property when adding the product to the cart.

### c. Show Rental Info in Cart

Finally, you'll customize the cart item component to show rental information for rentable products in the cart.

In `src/modules/cart/components/item/index.tsx`, add the following below the `LineItemOptions` component in the `return` statement of the `Item` component:

```tsx title="src/modules/cart/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue"
{!!item.metadata?.rental_start_date && !!item.metadata?.rental_end_date && (
  <Text className="txt-small text-ui-fg-muted">
    Rental: {new Date(item.metadata.rental_start_date as string).toLocaleDateString("en-US", { 
      month: "short", 
      day: "numeric", 
      year: "numeric", 
    })}
    {item.metadata.rental_days !== 1 && ` - ${new Date(item.metadata.rental_end_date as string).toLocaleDateString("en-US", { 
      month: "short", 
      day: "numeric", 
      year: "numeric", 
    })}`}
  </Text>
)}
```

You show the rental start and end dates if they're available in the line item's metadata.

### Test Adding Rental Products to Cart

You can now test adding rentable products to the cart in the Next.js Starter Storefront.

First, run both the Medusa server and the Next.js Starter Storefront.

Then, in the storefront, open the product details page for a rentable product. Select the rental start and end dates, then click the "Add to cart" button.

The product will be added to the cart with the rental options. You can click the cart icon at the top right to view the cart, where you'll see the rental dates displayed under the product name.

![Rental product added to cart in the storefront](https://res.cloudinary.com/dza7lstvk/image/upload/v1761660048/Medusa%20Resources/CleanShot_2025-10-28_at_15.38.24_2x_yfo50v.png)

***

## Step 11: Create Rental Orders

In this step, you'll implement the logic to create rental orders in the Medusa application. You'll wrap Medusa's existing order creation logic to handle rental-specific data and validation.

You'll create a workflow with the logic to create rental orders and an API route that uses this workflow.

### a. Create Rental Orders Workflow

First, you'll create a workflow that contains the logic to create rental orders, with support for rental products.

The workflow will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve cart details.
- [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md): Acquire a lock on the cart to prevent race conditions.
- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md): Complete the cart and create the order.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve order details.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve existing rentals for the order to ensure idempotency.
- [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md): Release the lock on the cart.

You'll implement the `validateRentalStep` and `createRentalsStep` steps used in the workflow. The rest are provided by Medusa out-of-the-box.

#### validateRentalStep

The `validateRentalStep` validates the rental items in the cart before creating the order. The validation logic is similar to the `validateRentalCartItemStep`.

To create the step, create the file `src/workflows/steps/validate-rental.ts` with the following content:

```ts title="src/workflows/steps/validate-rental.ts" badgeLabel="Medusa Application" badgeColor="green"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { MedusaError } from "@medusajs/framework/utils"
import { RENTAL_MODULE } from "../../modules/rental"
import RentalModuleService from "../../modules/rental/service"
import { InferTypeOf } from "@medusajs/framework/types"
import { RentalConfiguration } from "../../modules/rental/models/rental-configuration"
import hasCartOverlap from "../../utils/has-cart-overlap"
import validateRentalDates from "../../utils/validate-rental-dates"
import { cancelOrderWorkflow } from "@medusajs/medusa/core-flows"

export type ValidateRentalInput = {
  rental_items: {
    line_item_id: string
    variant_id: string
    quantity: number
    rental_configuration: InferTypeOf<typeof RentalConfiguration>
    rental_start_date: Date
    rental_end_date: Date
    rental_days: number
    order_id: string
  }[]
}

export const validateRentalStep = createStep(
  "validate-rental",
  async ({ rental_items, order_id }: ValidateRentalInput, { container }) => {
    const rentalModuleService: RentalModuleService = container.resolve(RENTAL_MODULE)

    for (let i = 0; i < rental_items.length; i++) {
      const rentalItem = rental_items[i]
      const { 
        line_item_id, 
        variant_id, 
        quantity, 
        rental_configuration, 
        rental_start_date, 
        rental_end_date, 
        rental_days,
      } = rentalItem

      if (rental_configuration.status !== "active") {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          `Rental configuration for variant ${variant_id} is not active`
        )
      }

      // Validate quantity is 1 for rental items
      if (quantity !== 1) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          `Rental items must have a quantity of 1. Line item ${line_item_id} has quantity ${quantity}`
        )
      }

      // Validate metadata presence
      if (!rental_start_date || !rental_end_date || !rental_days) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          `Line item ${line_item_id} is for a rentable product but is missing required metadata: rental_start_date, rental_end_date, and/or rental_days`
        )
      }

      // Convert to Date if needed
      const startDate = rental_start_date instanceof Date ? rental_start_date : new Date(rental_start_date)
      const endDate = rental_end_date instanceof Date ? rental_end_date : new Date(rental_end_date)
      
      validateRentalDates(
        startDate, 
        endDate, 
        {
          min_rental_days: rental_configuration.min_rental_days,
          max_rental_days: rental_configuration.max_rental_days,
        }, 
        rental_days
      )

      const hasCartOverlapResult = hasCartOverlap(
        {
          variant_id,
          rental_start_date,
          rental_end_date,
          rental_days,
        },
        rental_items.slice(i + 1).map((item) => ({
          id: item.line_item_id,
          variant_id: item.variant_id,
          metadata: {
            rental_start_date: item.rental_start_date.toISOString(),
            rental_end_date: item.rental_end_date.toISOString(),
            rental_days: item.rental_days,
          },
        }))
      )

      if (hasCartOverlapResult) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          `Cannot have multiple rental items for variant ${variant_id} with overlapping dates in the cart`
        )
      }

      if (await rentalModuleService.hasRentalOverlap(variant_id, startDate, endDate)) {
        throw new MedusaError(
          MedusaError.Types.NOT_ALLOWED,
          `Variant ${variant_id} is already rented during the requested period (${startDate.toISOString()} to ${endDate.toISOString()})`
        )
      }
    }

    return new StepResponse({ validated: true, order_id })
  },
  async (order_id, { container, context }) => {
    if (!order_id) {return}

    cancelOrderWorkflow(container).run({
      input: {
        order_id,
      },
      context,
      container,
    })
  }
)
```

The `validateRentalStep` accepts an array of rental items in the cart.

In the step, you perform similar validations as in the `validateRentalCartItemStep`, but this time for all rental items in the cart.

You validate that the rental configuration is active, the quantity is `1`, and the necessary rental metadata is present.

You also check for overlaps between rental items in the cart and existing rentals for the same variant.

If any validation fails, you throw an appropriate error. If all validations pass, you return a `StepResponse` indicating success.

You also provide a compensation function that cancels the order if the validation fails after the order has been created.

#### createRentalsForOrderStep

The `createRentalsForOrderStep` creates rental records for rental items in the order after it has been created.

To create the step, create the file `src/workflows/steps/create-rentals-for-order.ts` with the following content:

```ts title="src/workflows/steps/create-rentals-for-order.ts" badgeLabel="Medusa Application" badgeColor="green"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { RENTAL_MODULE } from "../../modules/rental"
import RentalModuleService from "../../modules/rental/service"
import { OrderDTO } from "@medusajs/framework/types"

export type CreateRentalsForOrderInput = {
  order: OrderDTO
}

export const createRentalsForOrderStep = createStep(
  "create-rentals-for-order",
  async ({ order }: CreateRentalsForOrderInput, { container }) => {
    const rentalModuleService: RentalModuleService = container.resolve(RENTAL_MODULE)

    const rentalItems = (order.items || []).filter((item) => {
      return item.metadata?.rental_start_date && 
        item.metadata?.rental_end_date && item.metadata?.rental_days
    })

    if (rentalItems.length === 0) {
      return new StepResponse([])
    }

    const rentals = await rentalModuleService.createRentals(
      rentalItems.map((item) => {
        const { 
          variant_id,
          metadata,
        } = item
        const rentalConfiguration = (item as any).variant?.product?.rental_configuration

        return {
          variant_id: variant_id!,
          customer_id: order.customer_id,
          order_id: order.id,
          line_item_id: item.id,
          rental_start_date: new Date(metadata?.rental_start_date as string),
          rental_end_date: new Date(metadata?.rental_end_date as string),
          rental_days: Number(metadata?.rental_days),
          rental_configuration_id: rentalConfiguration?.id as string,
        }
      })
    )

    return new StepResponse(
      rentals,
      rentals.map((rental) => rental.id)
    )
  },
  async (rentalIds, { container }) => {
    if (!rentalIds) {return}

    const rentalModuleService: RentalModuleService = container.resolve(RENTAL_MODULE)

    // Delete all created rentals on rollback
    await rentalModuleService.deleteRentals(rentalIds)
  }
)
```

The `createRentalsForOrderStep` accepts the order as input.

In the step, you filter the order items to find rental items based on the presence of rental metadata.

For each rental item, you create a rental record using the `createRentals` method from the Rental Module's service.

In the compensation function, you delete the created rentals if an error occurs during the workflow's execution.

#### Create Rentals Workflow

You can now create the `createRentalsWorkflow` that uses the above steps.

Create the file `src/workflows/create-rentals.ts` with the following content:

```ts title="src/workflows/create-rentals.ts" badgeLabel="Medusa Application" badgeColor="green" collapsibleLines="1-21" expandButtonLabel="Show Imports"
import { 
  createWorkflow, 
  WorkflowResponse, 
  transform,
  when,
} from "@medusajs/framework/workflows-sdk"
import { 
  acquireLockStep,
  completeCartWorkflow, 
  releaseLockStep, 
  useQueryGraphStep, 
} from "@medusajs/medusa/core-flows"
import { 
  ValidateRentalInput, 
  validateRentalStep,
} from "./steps/validate-rental"
import { 
  CreateRentalsForOrderInput, 
  createRentalsForOrderStep,
} from "./steps/create-rentals-for-order"

type CreateRentalsWorkflowInput = {
  cart_id: string
}

export const createRentalsWorkflow = createWorkflow(
  "create-rentals",
  ({ cart_id }: CreateRentalsWorkflowInput) => {
    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      fields: [
        "id",
        "customer_id",
        "items.*",
        "items.variant_id",
        "items.metadata",
        "items.variant.product.rental_configuration.*",
      ],
      filters: { id: cart_id },
      options: { throwIfKeyNotFound: true },
    })

    const rentalItems = transform({ carts }, ({ carts }) => {
      const cart = carts[0]
      const rentalItemsList: Record<string, unknown>[] = []

      for (const item of cart.items || []) {
        if (!item || !item.variant) {
          continue
        }

        const rentalConfig = (item.variant as any)?.product?.rental_configuration

        // Only include items that have an active rental configuration
        if (rentalConfig && rentalConfig.status === "active") {
          const metadata = item.metadata || {}

          rentalItemsList.push({
            line_item_id: item.id,
            variant_id: item.variant_id,
            quantity: item.quantity,
            rental_configuration: rentalConfig,
            rental_start_date: metadata.rental_start_date,
            rental_end_date: metadata.rental_end_date,
            rental_days: metadata.rental_days,
          })
        }
      }

      return rentalItemsList
    })

    acquireLockStep({
      key: cart_id,
      timeout: 2,
      ttl: 10,
    })

    const order = completeCartWorkflow.runAsStep({
      input: { id: cart_id },
    })

    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "id", 
        "items.*", 
        "customer_id", 
        "shipping_address.*", 
        "billing_address.*",
        "items.variant.product.rental_configuration.*",
      ],
      filters: { id: order.id },
      options: { throwIfKeyNotFound: true },
    }).config({ name: "retrieve-order" })

    const { data: rentals } = useQueryGraphStep({
      entity: "rental",
      fields: [
        "id",
      ],
      filters: { order_id: order.id },
    }).config({ name: "retrieve-rentals" })

    when(
      { rentals, rentalItems }, 
      (data) => data.rentals.length === 0 && data.rentalItems.length > 0
    )
    .then(() => {
      validateRentalStep({ 
        rental_items: rentalItems,
        order_id: order.id,
      } as unknown as ValidateRentalInput)
      createRentalsForOrderStep({
        order: orders[0],
      } as unknown as CreateRentalsForOrderInput)
    })

    releaseLockStep({
      key: cart_id,
    })

    // @ts-ignore
    return new WorkflowResponse({
      order: orders[0],
    })
  }
)
```

You create the `createRentalsWorkflow` workflow that accepts the cart ID as input.

In the workflow, you:

1. Retrieve the cart details using the `useQueryGraphStep`.
2. Extract the rental items from the cart.
3. Acquire a lock on the cart to prevent race conditions.
4. Complete the cart and create the order using the existing `completeCartWorkflow`.
5. Retrieve the created order details using the `useQueryGraphStep`.
6. Retrieve existing rentals for the order to ensure idempotency.
   - This is essential to avoid creating duplicate rentals if the workflow is retried.
7. Perform a condition with `when` to check that there are no existing rentals for the order and there are rental items in the cart. If the condition is met, you:
   1. Validate the rental items in the cart using the `validateRentalStep`.
   2. Create rental records for the rental items in the order using the `createRentalsForOrderStep`.
8. Release the lock on the cart.
9. Return the created order in the workflow response.

### b. Create Rental Orders API Route

Next, you'll create an API route that uses the `createRentalsWorkflow` to create rental orders.

Create the file `src/api/store/rentals/[cart_id]/route.ts` with the following content:

```ts title="src/api/store/rentals/[cart_id]/route.ts" badgeLabel="Medusa Application" badgeColor="green"
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { createRentalsWorkflow } from "../../../../workflows/create-rentals"

export const POST = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const { cart_id } = req.params

  const { result } = await createRentalsWorkflow(req.scope).run({
    input: {
      cart_id,
    },
  })

  res.json({
    type: "order",
    order: result.order,
  })
}
```

You expose a `POST` API route at `/store/rentals/{cart_id}`. In the route handler, you execute the `createRentalsWorkflow`, passing it the cart ID from the request parameters.

You return the created order in the response.

You'll use this API route in the storefront to create rental orders.

***

## Step 12: Create Rental Orders in Storefront

In this step, you'll customize the Next.js Starter Storefront to use the new rental order creation API route when placing an order.

### a. Update Place Order Function

First, you'll update the `placeOrder` function to use the rental order creation API route when placing an order.

In `src/lib/data/cart.ts`, find the `placeOrder` function and update the JS SDK call to the following:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
const cartRes = await sdk.client
  .fetch<{ type: "order"; order: HttpTypes.StoreOrder }>(
    `/store/rentals/${id}`,
    {
      method: "POST",
      headers,
    }
  )
// ...
```

You send a `POST` request to `/store/rentals/{cart_id}` to create the rental order.

Also, in the same function, remove the return statement that returns `cartRes.cart` to avoid TypeScript errors:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"]]}
export async function placeOrder(cartId?: string) {
  // ...
  
  // Remove this return statement
  // return cartRes.cart
}
```

### b. Show Rental Info in Order Confirmation

Next, you'll customize the order confirmation component to show rental information for rentable items in the order.

In `src/modules/order/components/item/index.tsx`, add the following below the `LineItemOptions` component in the `return` statement of the `Item` component:

```tsx title="src/modules/order/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue"
{!!item.metadata?.rental_start_date && !!item.metadata?.rental_end_date && (
  <Text className="txt-small text-ui-fg-muted">
    Rental: {new Date(item.metadata.rental_start_date as string).toLocaleDateString("en-US", { 
      month: "short", 
      day: "numeric", 
      year: "numeric", 
    })}
    {item.metadata.rental_days !== 1 && ` - ${new Date(item.metadata.rental_end_date as string).toLocaleDateString("en-US", { 
      month: "short", 
      day: "numeric", 
      year: "numeric", 
    })}`}
  </Text>
)}
```

You show the rental start and end dates if they're available in the line item's metadata.

### Test Creating Rental Orders

You can now test creating rental orders in the Next.js Starter Storefront.

First, run both the Medusa server and the Next.js Starter Storefront.

Then, in the storefront, open the cart that contains rental products. Proceed to checkout and complete the order.

After placing the order, you'll be redirected to the order confirmation page, where you'll see the rental dates displayed under the product name and options.

![Rental order confirmation in the storefront](https://res.cloudinary.com/dza7lstvk/image/upload/v1761661870/Medusa%20Resources/CleanShot_2025-10-28_at_16.30.14_2x_v8wbb7.png)

***

## Step 13: Manage Rentals in Admin

In this step, you'll allow admin users to manage rentals in the Medusa Admin Dashboard. You will:

1. Create an API route to retrieve rentals of an order.
2. Create a workflow to update a rental's status.
3. Create an API route to update a rental's status.
4. Inject an admin widget to view and manage rentals of an order.

### a. Retrieve Order Rentals API Route

First, you'll create an API route to retrieve the rentals associated with a specific order.

Create the file `src/api/admin/orders/[id]/rentals/route.ts` with the following content:

```ts title="src/api/admin/orders/[id]/rentals/route.ts" badgeLabel="Medusa Application" badgeColor="green"
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
  const { id } = req.params
  const query = req.scope.resolve("query")

  const { data: rentals } = await query.graph({
    entity: "rental",
    fields: [
      "*",
      "product_variant.id",
      "product_variant.title",
      "product_variant.product.id",
      "product_variant.product.title",
      "product_variant.product.thumbnail",
    ],
    filters: {
      order_id: id,
    },
  })

  res.json({ rentals })
}
```

You expose a `GET` API route at `/admin/orders/{id}/rentals`. In the route handler, you use Query to retrieve the rentals associated with the specified order ID.

### b. Update Rental Workflow

Next, you'll create a workflow to update a rental's status.

The workflow has a single step that updates the rental's status.

#### updateRentalStep

The `updateRentalStep` updates the rental's status with validation.

To create the step, create the file `src/workflows/steps/update-rental.ts` with the following content:

```ts title="src/workflows/steps/update-rental.ts" badgeLabel="Medusa Application" badgeColor="green"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { RENTAL_MODULE } from "../../modules/rental"
import RentalModuleService from "../../modules/rental/service"
import { MedusaError } from "@medusajs/framework/utils"

type UpdateRentalInput = {
  rental_id: string
  status: "active" | "returned" | "cancelled"
}

export const updateRentalStep = createStep(
  "update-rental",
  async ({ rental_id, status }: UpdateRentalInput, { container }) => {
    const rentalModuleService: RentalModuleService = container.resolve(RENTAL_MODULE)

    const existingRental = await rentalModuleService.retrieveRental(rental_id)
    const actualReturnDate = status === "returned" ? new Date() : null

    if (status === "active" && existingRental.status !== "pending") {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA, 
        "Can't activate a rental that is not in a pending state."
      )
    }

    if (status === "returned" && existingRental.status !== "active") {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Can't return a rental that is not in an active state."
      )
    }
    
    if (status === "cancelled" && !["active", "pending"].includes(existingRental.status)) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Can't cancel a rental that is not in an active or pending state."
      )
    }
    
    const updatedRental = await rentalModuleService.updateRentals({
      id: rental_id,
      status,
      actual_return_date: actualReturnDate,
    })

    return new StepResponse(updatedRental, existingRental)
  },
  async (existingRental, { container }) => {
    if (!existingRental) {return}

    const rentalModuleService: RentalModuleService = container.resolve(RENTAL_MODULE)

    await rentalModuleService.updateRentals({
      id: existingRental.id,
      status: existingRental.status,
      actual_return_date: existingRental.actual_return_date,
    })
  }
)
```

The `updateRentalStep` accepts the rental ID and the new status as input.

In the step, you retrieve the existing rental and validate that the status change is allowed based on the current status.

You then update the rental's status using the `updateRentals` method from the Rental Module's service.

In the compensation function, you revert the rental to its previous status if an error occurs during the workflow's execution.

#### Update Rental Workflow

Next, you'll create the `updateRentalWorkflow` that uses the above step.

Create the file `src/workflows/update-rental.ts` with the following content:

```ts title="src/workflows/update-rental.ts" badgeLabel="Medusa Application" badgeColor="green"
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { updateRentalStep } from "./steps/update-rental"

type UpdateRentalWorkflowInput = {
  rental_id: string
  status: "active" | "returned" | "cancelled"
}

export const updateRentalWorkflow = createWorkflow(
  "update-rental",
  ({ rental_id, status }: UpdateRentalWorkflowInput) => {
    // Update rental status
    const updatedRental = updateRentalStep({
      rental_id,
      status,
    })

    return new WorkflowResponse(updatedRental)
  }
)
```

You create the `updateRentalWorkflow` workflow that accepts the rental ID and the new status as input.

In the workflow, you update the rental's status using the `updateRentalStep` and return the updated rental in the workflow response.

### c. Update Rental API Route

Next, you'll create an API route that uses the `updateRentalWorkflow` to update a rental's status.

Create the file `src/api/admin/rentals/[id]/route.ts` with the following content:

```ts title="src/api/admin/rentals/[id]/route.ts" badgeLabel="Medusa Application" badgeColor="green"
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { updateRentalWorkflow } from "../../../../workflows/update-rental"
import { z } from "zod"

export const PostRentalStatusBodySchema = z.object({
  status: z.enum(["active", "returned", "cancelled"]),
})

export const POST = async (
  req: MedusaRequest<z.infer<typeof PostRentalStatusBodySchema>>,
  res: MedusaResponse
) => {
  const { id } = req.params
  const { status } = req.validatedBody

  const { result } = await updateRentalWorkflow(req.scope).run({
    input: {
      rental_id: id,
      status,
    },
  })

  res.json({ rental: result })
}
```

You create a Zod schema to validate the request body, which includes the new rental status.

You also expose a `POST` API route at `/admin/rentals/{id}`. In the route handler, you execute the `updateRentalWorkflow`, and return the updated rental in the response.

### d. Apply Validation Middleware

Next, you'll add validation middleware to ensure that the request body for updating a rental's status is valid.

In `src/api/middlewares.ts`, add the following import at the top of the file:

```ts title="src/api/middlewares.ts" badgeLabel="Medusa Application" badgeColor="green"
import { PostRentalStatusBodySchema } from "./admin/rentals/[id]/route"
```

Then, pass a new object to the `routes` array in `defineMiddlewares`:

```ts title="src/api/middlewares.ts" badgeLabel="Medusa Application" badgeColor="green"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/rentals/:id",
      methods: ["POST"],
      middlewares: [
        validateAndTransformBody(PostRentalStatusBodySchema),
      ],
    },
  ],
})
```

You apply the `validateAndTransformBody` middleware to the rental update route, using the `PostRentalStatusBodySchema` schema to validate incoming requests.

### e. Inject Admin Widget

Finally, you'll inject an admin widget into the order details page to view and manage rentals associated with the order.

Create the file `src/admin/widgets/order-rental-items.tsx` with the following content:

```tsx title="src/admin/widgets/order-rental-items.tsx" badgeLabel="Medusa Application" badgeColor="green" collapsibleLines="1-18" expandButtonLabel="Show Imports"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import {
  Container,
  Heading,
  Text,
  Button,
  Drawer,
  Label,
  Select,
  toast,
  Badge,
  Table,
} from "@medusajs/ui"
import { useQuery, useMutation } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"
import { DetailWidgetProps, AdminOrder } from "@medusajs/framework/types"
import { useEffect, useState } from "react"

type Rental = {
  id: string
  variant_id: string
  customer_id: string
  order_id: string
  line_item_id: string
  rental_start_date: string
  rental_end_date: string
  actual_return_date: string | null
  rental_days: number
  status: "pending" | "active" | "returned" | "cancelled"
  product_variant?: {
    id: string
    title: string
    product?: {
      id: string
      title: string
      thumbnail: string
    }
  }
}

type RentalsResponse = {
  rentals: Rental[]
}

const OrderRentalItemsWidget = ({
  data: order,
}: DetailWidgetProps<AdminOrder>) => {
  const [drawerOpen, setDrawerOpen] = useState(false)
  const [selectedRental, setSelectedRental] = useState<Rental | null>(null)
  const [newStatus, setNewStatus] = useState("")

  const { data, refetch } = useQuery<RentalsResponse>({
    queryFn: () =>
      sdk.client.fetch(
        `/admin/orders/${order.id}/rentals`
      ),
    queryKey: [["orders", order.id, "rentals"]],
  })

  useEffect(() => {
    if (data?.rentals.length) {
      setSelectedRental(data.rentals[0])
      setNewStatus(data.rentals[0].status)
    }
  }, [data?.rentals])

  // TODO add mutation
}

export const config = defineWidgetConfig({
  zone: "order.details.after",
})

export default OrderRentalItemsWidget
```

You create the `OrderRentalItemsWidget` component that will be injected into the order details page in the admin dashboard.

In the component, you define the following state variables:

- `drawerOpen`: Controls the visibility of the rental management drawer.
- `selectedRental`: Holds the currently selected rental for management.
- `newStatus`: Holds the new status selected for the rental being managed.

You also retrieve the rentals associated with the order, and set the initial selected rental and status when the data is loaded.

Next, you'll add a mutation for updating a rental's status. Replace the `// TODO add mutation` comment with the following code:

```tsx title="src/admin/widgets/order-rental-items.tsx" badgeLabel="Medusa Application" badgeColor="green"
const updateMutation = useMutation({
  mutationFn: async (params: { rentalId: string; status: string }) => {
    return sdk.client.fetch(`/admin/rentals/${params.rentalId}`, {
      method: "POST",
      body: { status: params.status },
    })
  },
  onSuccess: () => {
    toast.success("Rental status updated successfully")
    refetch()
    setDrawerOpen(false)
    setSelectedRental(null)
  },
  onError: (error) => {
    toast.error(`Failed to update rental status: ${error.message}`)
  },
})

// TODO add helper functions
```

The mutation sends a `POST` request to the rental update API route with the new status.

Next, you'll add helper functions to handle events and formatting. Replace the `// TODO add helper functions` comment with the following code:

```tsx title="src/admin/widgets/order-rental-items.tsx" badgeLabel="Medusa Application" badgeColor="green"
const handleOpenDrawer = (rental: Rental) => {
  setSelectedRental(rental)
  setNewStatus(rental.status)
  setDrawerOpen(true)
}

const handleSubmit = () => {
  if (!selectedRental) {
    return
  }

  updateMutation.mutate({
    rentalId: selectedRental.id,
    status: newStatus,
  })
}

const getStatusBadgeColor = (status: string) => {
  switch (status) {
    case "active":
      return "green"
    case "returned":
      return "blue"
    case "cancelled":
      return "red"
    case "pending":
      return "orange"
    default:
      return "grey"
  }
}

const formatStatus = (status: string) => {
  return status.charAt(0).toUpperCase() + status.slice(1)
}

const formatDate = (dateString: string) => {
  return new Date(dateString).toLocaleDateString()
}

// TODO add return statement
```

You define the following functions:

- `handleOpenDrawer`: Opens the rental management drawer for the selected rental.
- `handleSubmit`: Submits the rental status update.
- `getStatusBadgeColor`: Returns the badge color based on the rental status.
- `formatStatus`: Formats the rental status string.
- `formatDate`: Formats a date string into a readable format.

Finally, you'll add the return statement to render the component UI. Replace the `// TODO add return statement` comment with the following code:

```tsx title="src/admin/widgets/order-rental-items.tsx" badgeLabel="Medusa Application" badgeColor="green"
if (!data?.rentals.length) {
  return null
}

return (
  <>
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">Rental Items</Heading>
      </div>
      <Table>
        <Table.Header>
          <Table.Row>
            <Table.HeaderCell>Product</Table.HeaderCell>
            <Table.HeaderCell>Start Date</Table.HeaderCell>
            <Table.HeaderCell>End Date</Table.HeaderCell>
            <Table.HeaderCell>Status</Table.HeaderCell>
            <Table.HeaderCell>Actions</Table.HeaderCell>
          </Table.Row>
        </Table.Header>
        <Table.Body>
          {data.rentals.map((rental) => (
            <Table.Row key={rental.id}>
              <Table.Cell className="py-4">
                <div className="flex items-start gap-4">
                  {rental.product_variant?.product?.thumbnail && (
                    <img
                      src={rental.product_variant?.product?.thumbnail || ""}
                      alt={rental.product_variant?.product?.title || ""}
                      className="w-6 h-8 object-cover rounded border border-ui-border-base"
                    />
                  )}
                  <div>
                    <Text weight="plus" size="small" className="text-ui-fg-base">
                      {rental.product_variant?.product?.title || "N/A"}
                    </Text>
                    <Text size="xsmall" className="text-ui-fg-subtle">
                      {rental.product_variant?.title || "N/A"}
                    </Text>
                  </div>
                </div>
              </Table.Cell>
              <Table.Cell>
                {formatDate(rental.rental_start_date)}
              </Table.Cell>
              <Table.Cell>
                {formatDate(rental.rental_end_date)}
              </Table.Cell>
              <Table.Cell>
                <Badge color={getStatusBadgeColor(rental.status)} size="2xsmall">
                  {formatStatus(rental.status)}
                </Badge>
              </Table.Cell>
              <Table.Cell>
                <Button
                  size="small"
                  variant="transparent"
                  onClick={() => handleOpenDrawer(rental)}
                  className="p-0 text-ui-fg-subtle"
                >
                  Update Status
                </Button>
              </Table.Cell>
            </Table.Row>
          ))}
        </Table.Body>
      </Table>
    </Container>

    <Drawer open={drawerOpen} onOpenChange={setDrawerOpen}>
      <Drawer.Content>
        <Drawer.Header>
          <Drawer.Title>Update Rental Status</Drawer.Title>
        </Drawer.Header>
        <Drawer.Body className="space-y-4">
          {selectedRental && (
            <>
              <div>
                <Text weight="plus" className="mb-2">
                  Rental Details
                </Text>
                <div className="space-y-1">
                  <Text size="small">
                    Product:{" "}
                    {selectedRental.product_variant?.product?.title || "N/A"}
                  </Text>
                  <Text size="small">
                    Variant: {selectedRental.product_variant?.title || "N/A"}
                  </Text>
                  <Text size="small">
                    Rental Period: {formatDate(selectedRental.rental_start_date)} to{" "}
                    {formatDate(selectedRental.rental_end_date)} ({selectedRental.rental_days}{" "}
                    days)
                  </Text>
                </div>
              </div>
              <hr />
              <div className="space-y-1">
                <Label htmlFor="status" className="txt-compact-small font-medium">Status</Label>
                <Select value={newStatus} onValueChange={setNewStatus}>
                  <Select.Trigger id="status">
                    <Select.Value />
                  </Select.Trigger>
                  <Select.Content>
                    <Select.Item value="pending" disabled className="text-ui-fg-disabled">Pending</Select.Item>
                    <Select.Item value="active">Active</Select.Item>
                    <Select.Item value="returned">Returned</Select.Item>
                    <Select.Item value="cancelled">Cancelled</Select.Item>
                  </Select.Content>
                </Select>
              </div>
            </>
          )}
        </Drawer.Body>
        <Drawer.Footer>
          <div className="flex gap-2">
            <Button
              variant="secondary"
              onClick={() => setDrawerOpen(false)}
            >
              Cancel
            </Button>
            <Button
              onClick={handleSubmit}
              disabled={updateMutation.isPending || newStatus === selectedRental?.status}
              isLoading={updateMutation.isPending}
            >
              Save
            </Button>
          </div>
        </Drawer.Footer>
      </Drawer.Content>
    </Drawer>
  </>
)
```

If there are no rentals, you return `null` to avoid rendering the widget.

Otherwise, you show a table of rental items associated with the order, along with a button to update the status of each rental.

When the "Update Status" button is clicked, a drawer opens, allowing the admin user to change the rental's status.

### Test Managing Rentals in Admin

You can now test managing rentals in the Medusa Admin Dashboard.

First, start the Medusa application and log in.

Then, go to Orders and click on an order that contains rental items.

In the order details page, you'll see a new "Rental Items" section with a table listing the rental items associated with the order.

![Rental items in admin order details page](https://res.cloudinary.com/dza7lstvk/image/upload/v1761662993/Medusa%20Resources/CleanShot_2025-10-28_at_16.49.29_2x_usufnn.png)

You can click the "Update Status" button for a rental item to open the drawer and edit its status.

![Update rental status drawer in admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1761663069/Medusa%20Resources/CleanShot_2025-10-28_at_16.50.35_2x_bzmaoq.png)

You can change the rental status and save the changes. The rental status will be updated accordingly.

***

## Step 14: Handle Order Cancellation

Medusa Admin users can cancel orders. So, in this step, you'll customize the order cancellation flow to validate that rental items in the order can be cancelled based on their rental status. You'll also update the rental statuses when an order is cancelled.

### a. Validate Rental Items on Order Cancellation

To add custom validation when cancelling an order, you'll consume the `orderCanceled` hook of the `cancelOrderWorkflow`. A [workflow hook](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md) allows you to run custom steps at specific points in a workflow.

To consume the `orderCanceled` hook, create the file `src/workflows/hooks/validate-order-cancel.ts` with the following content:

```ts title="src/workflows/hooks/validate-order-cancel.ts" badgeLabel="Medusa Application" badgeColor="green"
import { cancelOrderWorkflow } from "@medusajs/medusa/core-flows"
import { MedusaError, ContainerRegistrationKeys } from "@medusajs/framework/utils"

cancelOrderWorkflow.hooks.orderCanceled(
  async ({ order }, { container }) => {
    const query = container.resolve(ContainerRegistrationKeys.QUERY)

    // Retrieve all rentals associated with this order
    const { data: rentals } = await query.graph({
      entity: "rental",
      fields: ["id", "status", "variant_id"],
      filters: {
        order_id: order.id,
      },
    })

    // Validate that all rentals are in a cancelable state
    // Only pending, active, or already cancelled rentals can be part of a canceled order
    const nonCancelableRentals = rentals.filter(
      (rental: any) => !["pending", "active", "cancelled"].includes(rental.status)
    )

    if (nonCancelableRentals.length > 0) {
      const problematicRentals = nonCancelableRentals
        .map((r: any) => `${r.id} (${r.status})`)
        .join(", ")
      throw new MedusaError(
        MedusaError.Types.NOT_ALLOWED,
        `Cannot cancel order. Some rentals cannot be canceled: ${problematicRentals}. Only rentals with status "pending", "active", or "cancelled" can be canceled with the order.`
      )
    }
  }
)
```

You consume the `orderCanceled` hook of the `cancelOrderWorkflow`, passing it a step function.

In the subscriber function, you retrieve all rentals associated with the order being cancelled. If a rental's status is `returned`, you throw an error. This will roll back the changes made by the `cancelOrderWorkflow`, preventing the order from being cancelled.

#### Test Order Cancellation Validation

To test the order cancellation validation, try to cancel an order from the Medusa Admin that has rental items with `returned` status. The order cancellation should fail.

### b. Update Rental Statuses on Order Cancellation

Next, you'll update the rental statuses when an order is cancelled.

When an order is cancelled, Medusa emits an `order.canceled` event. You can handle this event in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).

A subscriber is an asynchronous function that executes actions in the background when specific events are emitted.

To create the subscriber, create the file `src/subscribers/order-canceled.ts` with the following content:

```ts title="src/subscribers/order-canceled.ts" badgeLabel="Medusa Application" badgeColor="green"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { updateRentalWorkflow } from "../workflows/update-rental"

export default async function orderCanceledHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const logger = container.resolve("logger")
  const query = container.resolve("query")

  logger.info(`Processing rental cancellations for order ${data.id}`)

  try {
    // Retrieve all rentals associated with the canceled order
    const { data: rentals } = await query.graph({
      entity: "rental",
      fields: ["id", "status"],
      filters: {
        order_id: data.id,
        status: {
          $ne: "cancelled",
        },
      },
    })

    if (!rentals || rentals.length === 0) {
      logger.info(`No rentals found for order ${data.id}`)
      return
    }

    logger.info(`Found ${rentals.length} rental(s) to cancel for order ${data.id}`)

    // Update each rental's status to cancelled
    let successCount = 0
    let errorCount = 0

    for (const rental of rentals) {
      try {
        await updateRentalWorkflow(container).run({
          input: {
            rental_id: (rental as any).id,
            status: "cancelled",
          },
        })
        successCount++
        logger.info(`Cancelled rental ${(rental as any).id}`)
      } catch (error) {
        errorCount++
        logger.error(
          `Failed to cancel rental ${(rental as any).id}: ${error.message}`
        )
      }
    }

    logger.info(
      `Rental cancellation complete for order ${data.id}: ${successCount} succeeded, ${errorCount} failed`
    )
  } catch (error) {
    logger.error(`Error in orderCanceledHandler: ${error.message}`)
  }
}

export const config: SubscriberConfig = {
  event: "order.canceled",
}
```

A subscriber file must export:

- An asynchronous function that is executed when its associated event is emitted.
- An object that indicates the event that the subscriber is listening to.

In the subscriber function, you retrieve all rentals associated with the cancelled order that are not already cancelled.

Then, you iterate over the rentals and update their status to `cancelled` using the `updateRentalWorkflow`.

#### Test Rental Status Update on Order Cancellation

To test the rental status update on order cancellation, cancel an order from the Medusa Admin that has rental items with `pending` or `active` status.

Then, refresh the page. You'll see that the rental items' statuses have been updated to `cancelled`.

![Cancelled rentals in admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1761665415/Medusa%20Resources/CleanShot_2025-10-28_at_17.29.55_2x_apkc5n.png)

***

## Optional: Automate Rental Status Updates

In realistic scenarios, you might want to automate rental status updates based on specific events, rental periods, or external triggers.

In this optional step, you'll explore two approaches to automate rental status updates:

1. Using a [scheduled job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md) to periodically check and update rental statuses.
2. Handling events like `shipment.created` to update rental statuses when shipments are created.

You can also implement other approaches based on your use case.

### a. Scheduled Job for Rental Status Updates

A scheduled job is an asynchronous function that runs tasks at specific intervals while the Medusa application is running. You can use scheduled jobs to change a rental's status based on the rental period.

For example, to automatically mark rentals as `active` when their rental start date is reached, create a scheduled job at `src/jobs/activate-rentals.ts` with the following content:

```ts title="src/jobs/activate-rentals.ts" badgeLabel="Medusa Application" badgeColor="green"
import { MedusaContainer } from "@medusajs/framework/types"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { updateRentalWorkflow } from "../workflows/update-rental"

export default async function activateRentalsJob(container: MedusaContainer) {
  const logger = container.resolve("logger")
  const query = container.resolve(ContainerRegistrationKeys.QUERY)

  // Get current date at start of day for comparison
  const today = new Date()
  today.setHours(0, 0, 0, 0)

  // Get tomorrow at start of day
  const tomorrow = new Date(today)
  tomorrow.setDate(tomorrow.getDate() + 1)

  try {
    // Find all pending rentals whose start date is today
    const { data: rentalsToActivate } = await query.graph({
      entity: "rental",
      fields: ["id", "rental_start_date", "status"],
      filters: {
        status: ["pending"],
        rental_start_date: {
          $gte: today,
          $lt: tomorrow,
        },
      },
    })

    if (rentalsToActivate.length === 0) {
      logger.info("No pending rentals to activate today")
      return
    }

    logger.info(`Found ${rentalsToActivate.length} rentals to activate today`)

    // Activate each rental using the workflow
    let successCount = 0
    let errorCount = 0

    for (const rental of rentalsToActivate) {
      try {
        await updateRentalWorkflow(container).run({
          input: {
            rental_id: rental.id,
            status: "active",
          },
        })
        successCount++
        logger.info(`Activated rental ${rental.id}`)
      } catch (error) {
        errorCount++
        logger.error(`Failed to activate rental ${rental.id}: ${error.message}`)
      }
    }

    logger.info(
      `Rental activation complete: ${successCount} succeeded, ${errorCount} failed`
    )
  } catch (error) {
    logger.error(`Error in rental activation job: ${error.message}`)
  }
}

export const config = {
  name: "activate-rentals",
  schedule: "0 0 * * *", // Every day at midnight
}
```

A scheduled job file must export:

- An asynchronous function that is executed at the specified interval in the configuration object.
- A configuration object that specifies when to execute the scheduled job. The schedule is defined as a cron pattern.

This scheduled job runs every day at midnight. In the job function, you retrieve all rentals with `pending` status whose rental start date is the current date.

Then, you update their status to `active` using the `updateRentalWorkflow`.

#### Test Scheduled Job

To test the scheduled job, you can change its `schedule` property to run every minute:

```ts title="src/jobs/activate-rentals.ts" badgeLabel="Medusa Application" badgeColor="green"
export const config = {
  name: "activate-rentals",
  schedule: "*/1 * * * *", // Every minute
}
```

Then, start the Medusa application and wait for a minute. You should see log messages indicating that the job is running and has activated any pending rentals whose start date is today.

### b. Event-Driven Rental Status Updates

You can also update rental statuses based on specific [events](https://docs.medusajs.com/references/events/index.html.md) in Medusa.

For example, you might want to mark rentals as `active` when their associated shipments are created. You can do this by creating a subscriber that listens to the `shipment.created` event:

```ts title="src/subscribers/shipment-created.ts" badgeLabel="Medusa Application" badgeColor="green"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { updateRentalWorkflow } from "../workflows/update-rental"

export default async function shipmentCreatedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string; no_notification?: boolean }>) {
  const logger = container.resolve("logger")
  const query = container.resolve(ContainerRegistrationKeys.QUERY)

  logger.info(`Processing rental activations for shipment ${data.id}`)

  try {
    // Retrieve the fulfillment with its items
    const { data: fulfillments } = await query.graph({
      entity: "fulfillment",
      fields: ["id", "items.*", "items.line_item_id"],
      filters: {
        id: data.id,
      },
    })

    if (!fulfillments || fulfillments.length === 0) {
      logger.warn(`Fulfillment ${data.id} not found`)
      return
    }

    const fulfillment = fulfillments[0]
    const lineItemIds = (fulfillment as any).items?.map((item: any) => item.line_item_id) || []

    if (lineItemIds.length === 0) {
      logger.info(`No items found in fulfillment ${data.id}`)
      return
    }

    logger.info(`Found ${lineItemIds.length} item(s) in fulfillment ${data.id}`)

    // Retrieve all rentals associated with these line items
    const { data: rentals } = await query.graph({
      entity: "rental",
      fields: ["id", "status", "line_item_id", "variant_id"],
      filters: {
        line_item_id: lineItemIds,
        status: "pending",
      },
    })

    if (!rentals || rentals.length === 0) {
      logger.info(`No rentals found for fulfillment ${data.id}`)
      return
    }

    logger.info(`Found ${rentals.length} rental(s) to activate for fulfillment ${data.id}`)

    // Update each rental's status to active
    let successCount = 0
    let errorCount = 0

    for (const rental of rentals) {      
      try {
        await updateRentalWorkflow(container).run({
          input: {
            rental_id: rental.id,
            status: "active",
          },
        })
        successCount++
        logger.info(`Activated rental ${rental.id} (variant: ${rental.variant_id})`)
      } catch (error) {
        errorCount++
        logger.error(
          `Failed to activate rental ${rental.id}: ${error.message}`
        )
      }
    }

    logger.info(
      `Rental activation complete for shipment ${data.id}: ${successCount} activated, ${errorCount} failed`
    )
  } catch (error) {
    logger.error(`Error in shipmentCreatedHandler: ${error.message}`)
  }
}

export const config: SubscriberConfig = {
  event: "shipment.created",
}
```

You create a subscriber that listens to the `shipment.created` event. In the subscriber function, you:

1. Retrieve the fulfillment associated with the shipment.
2. Extract the line item IDs from the fulfillment.
3. Retrieve all rentals associated with those line items that are in `pending` status.
4. Update their status to `active` using the `updateRentalWorkflow`.

#### Test Event-Driven Rental Status Update

To test the event-driven rental status update, create a fulfillment for an order that has rental items with `pending` status from the Medusa Admin. Then, mark the fulfillment as shipped.

If you refresh the order details page, you should see that the rental items' statuses have been updated to `active`.

![Activated rentals after shipment creation in admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1761665929/Medusa%20Resources/CleanShot_2025-10-28_at_17.38.04_2x_xgwwan.png)

***

## Optional: Remove Shipping for Rental Items

In some scenarios, a rentable item might not require shipping. For example, if the rental item is digital.

You can remove the shipping requirement for rentable items by:

1. Removing the associated shipping profile of the product. You can do this from the Medusa Admin.
2. Customizing the checkout flow in the storefront to remove the delivery step for orders that only contain rentable items.

Learn more about customizing shipping requirements for products in the [Selling Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/selling-products/index.html.md) guide.

***

## Optional: Handling Inventory for Rental Items

Medusa provides optional [inventory management for product variants](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md). If enabled, Medusa will increment and decrement inventory levels for product variants when orders are placed, fulfilled, or returned.

Handling inventory for rental products depends on your specific use case:

1. No inventory management: If inventory of rental products is not a concern, you can disable inventory management for rental product variants. The product variants will always be considered in-stock, and only the rental logic will govern their availability.
2. Standard inventory management: If you want to manage inventory for rental products, you can enable inventory management for rental product variants. In this case, you'll need to ensure that inventory levels are adjusted appropriately based on rental status changes.

For example, in the `updateRentalWorkflow`, you might want to increment inventory when the rental is marked as `returned`:

Medusa creates a reservation for the inventory of a product variant when an order is placed, and decrements the inventory when the order is fulfilled. So, you don't need to manually adjust inventory when a rental is created or activated.

```ts title="src/workflows/update-rental.ts" badgeLabel="Medusa Application" badgeColor="green"
import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { updateRentalStep } from "./steps/update-rental"
import { adjustInventoryLevelsStep, useQueryGraphStep } from "@medusajs/medusa/core-flows"

type UpdateRentalWorkflowInput = {
  rental_id: string
  status: "active" | "returned" | "cancelled"
}

export const updateRentalWorkflow = createWorkflow(
  "update-rental",
  ({ rental_id, status }: UpdateRentalWorkflowInput) => {
    // Update rental status
    const updatedRental = updateRentalStep({
      rental_id,
      status,
    })
    
    when({ updatedRental }, (data) => data.updatedRental.status === "returned" )
      .then(() => {
        // Retrieve variant inventory details
        const { data: variants } = useQueryGraphStep({
          entity: "variant",
          fields: [
            "inventory.*",
            "inventory.location_levels.*",
          ],
          filters: {
            id: updatedRental.variant_id,
          },
        })

        // Prepare inventory adjustment
        const stockUpdate = transform({
          variants,
        }, (data) => {
          const inventoryUpdates: {
            inventory_item_id: string
            location_id: string
            adjustment: number
          }[] = []

          data.variants[0].inventory?.map((inv) => {
            inv?.location_levels?.map((locLevel) => {
              inventoryUpdates.push({
                inventory_item_id: inv!.id,
                location_id: locLevel!.location_id,
                adjustment: 1,
              })
            })
          })

          return inventoryUpdates
        })

        // Adjust inventory levels
        adjustInventoryLevelsStep(stockUpdate)
      })

    return new WorkflowResponse(updatedRental)
  }
)
```

***

## Next Steps

You have now implemented product rentals in your Medusa application. You can expand on this foundation by adding more features, such as allowing customers to view and manage their rentals from the storefront.

### Learn More about Medusa

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.


# Implement Product Reviews in Medusa

In this tutorial, you'll learn how to implement product reviews in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) which are available out-of-the-box. The features include product-management features.

Medusa doesn't provide product reviews out-of-the-box, but the Medusa Framework facilitates implementing customizations like product reviews. In this tutorial, you'll learn how to customize the Medusa server, Admin dashboard, and Next.js Starter Storefront to implement product reviews.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer.

## Summary

By following this tutorial, you'll learn how to:

- Install and set up Medusa.
- Define product reviews models and implement their management features in the Medusa server.
- Customize the Medusa Admin to allow merchants to view and manage product reviews.
- Customize the Next.js Starter Storefront to display product reviews and allow customers to submit reviews.

![Diagram showcasing the product review features in the storefront and admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741941058/Medusa%20Resources/reviews-overview_nufybf.jpg)

- [Product Reviews Repository](https://github.com/medusajs/examples/tree/main/product-reviews): Find the full code for this guide in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1741941475/OpenApi/product-reviews_jh8ohj.yaml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Add Product Review Module

In Medusa, you can build custom features in a [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

In the module, you define the data models necessary for a feature and the logic to manage these data models. Later, you can build commerce flows around your module.

In this step, you'll build a Product Review Module that defines the necessary data models to store and manage product reviews.

Refer to the [Modules documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to learn more.

### Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/product-review`.

### Create Data Models

A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

Refer to the [Data Models documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md) to learn more.

For the Product Review Module, you need to define a `Review` data model that represents a product review. So, create the file `src/modules/product-review/models/review.ts` with the following content:

```ts title="src/modules/product-review/models/review.ts"
import { model } from "@medusajs/framework/utils"

const Review = model.define("review", {
  id: model.id().primaryKey(),
  title: model.text().nullable(),
  content: model.text(),
  rating: model.float(),
  first_name: model.text(),
  last_name: model.text(),
  status: model.enum(["pending", "approved", "rejected"]).default("pending"),
  product_id: model.text().index("IDX_REVIEW_PRODUCT_ID"),
  customer_id: model.text().nullable(),
})
.checks([
  {
    name: "rating_range", 
    expression: (columns) => `${columns.rating} >= 1 AND ${columns.rating} <= 5`,
  },
])

export default Review
```

You define the `Review` data model using the `model.define` method of the DML. It accepts the data model's table name as a first parameter, and the model's schema object as a second parameter.

The `Review` data model has the following properties:

- `id`: A unique ID for the review.
- `title`: The review's title.
- `content`: The review's content.
- `rating`: The review's rating. You also add a [check constraint](https://docs.medusajs.com/docs/learn/fundamentals/data-models/check-constraints/index.html.md) to ensure the rating is between 1 and 5.
- `first_name`: The first name of the reviewer.
- `last_name`: The last name of the reviewer.
- `status`: The review's status, which can be `pending`, `approved`, or `rejected`.
- `product_id`: The ID of the product the review is for.
- `customer_id`: The ID of the customer who submitted the review.

Learn more about defining data model properties in the [Property Types documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md).

### Create Module's Service

You now have the necessary data model in the Review Module, but you'll need to manage its records. You do this by creating a service in the module.

A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, allowing you to manage your data models, or connect to a third-party service, which is useful if you're integrating with external services.

Refer to the [Module Service documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md) to learn more.

To create the Review Module's service, create the file `src/modules/product-review/service.ts` with the following content:

```ts title="src/modules/product-review/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import Review from "./models/review"

class ProductReviewModuleService extends MedusaService({
  Review,
}) {
}

export default ProductReviewModuleService
```

The `ProductReviewModuleService` extends `MedusaService` from the Modules SDK which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods.

So, the `ProductReviewModuleService` class now has methods like `createReviews` and `retrieveReview`.

Find all methods generated by the `MedusaService` in [the Service Factory reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md).

You'll use this service later when you implement custom flows for product reviews.

### Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/product-review/index.ts` with the following content:

```ts title="src/modules/product-review/index.ts"
import { Module } from "@medusajs/framework/utils"
import ProductReviewModuleService from "./service"

export const PRODUCT_REVIEW_MODULE = "productReview"

export default Module(PRODUCT_REVIEW_MODULE, {
  service: ProductReviewModuleService,
})
```

You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `productReview`.
2. An object with a required property `service` indicating the module's service.

You also export the module's name as `PRODUCT_REVIEW_MODULE` so you can reference it later.

### Add Module to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/product-review",
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

### Generate Migrations

Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module.

Refer to the [Migrations documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md) to learn more.

Medusa's CLI tool can generate the migrations for you. To generate a migration for the Review Module, run the following command in your Medusa application's directory:

```bash
npx medusa db:generate productReview
```

The `db:generate` command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a `migrations` directory under `src/modules/product-review` that holds the generated migration.

Then, to reflect these migrations on the database, run the following command:

```bash
npx medusa db:migrate
```

The table for the `Review` data model is now created in the database.

***

## Step 3: Define Review \<> Product Link

When you defined the `Review` data model, you added properties that store the ID of records managed by other modules. For example, the `product_id` property stores the ID of the product this review is for, but products are managed by the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md).

Medusa integrates modules into your application without implications or side effects by isolating modules from one another. This means you can't directly create relationships between data models in your module and data models in other modules.

Instead, Medusa provides the mechanism to define links between data models, and retrieve and manage linked records while maintaining module isolation. Links are useful to define associations between data models in different modules, or extend a model in another module to associate custom properties with it.

Refer to the [Module Isolation documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md) to learn more.

In this step, you'll define a link between the Product Review Module's `Review` data model, and the Product Module's `Product` data model. You'll then use this link to retrieve the product associated with a review.

You can also define a link between the `Review` data model and the `Customer` data model to retrieve the customer who submitted the review in a similar manner.

You can define links between data models in a TypeScript or JavaScript file under the `src/links` directory. So, create the file `src/links/review-product.ts` with the following content:

```ts title="src/links/review-product.ts"
import { defineLink } from "@medusajs/framework/utils"
import ProductReviewModule from "../modules/product-review"
import ProductModule from "@medusajs/medusa/product"

export default defineLink(
  {
    linkable: ProductReviewModule.linkable.review,
    field: "product_id",
    isList: false,
  },
  ProductModule.linkable.product,
  {
    readOnly: true,
  }
)
```

You define a link using the `defineLink` function from the Modules SDK. It accepts three parameters:

1. An object indicating the first data model part of the link. A module has a special `linkable` property that contains link configurations for its data models. So, you can pass the link configurations for the `Review` data model from the Product Review module, specifying that its `product_id` property holds the ID of the linked record. You also specify `isList` as `false` since a review can only have one product.
2. An object indicating the second data model part of the link. You pass the linkable configurations of the Product Module's `Product` data model.
3. An optional object with additional configurations for the link. By default, Medusa creates a table in the database to represent the link you define. However, when you only want to retrieve the linked records without managing and storing the links, you can set the `readOnly` option to `true`.

You can now retrieve the product of a review, as you'll see in later steps.

***

## Step 4: Create Review Workflow

You're now ready to start implementing product-review features. The first one you'll implement is the ability for customers to create a product review.

To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an endpoint.

So, in this section, you'll learn how to create a workflow that creates a review. Later, you'll execute this workflow in an API route.

Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

The workflow will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the product to confirm it exists.
- [createReviewStep](#createReviewStep): Create the review.

The `useQueryGraphStep` step is provided by Medusa in its `@medusajs/medusa/core-flows` package. So, you only need to implement the `createReviewStep` step.

### createReviewStep

In the second step of the workflow, you create the review. To create a step, create the file `src/workflows/steps/create-review.ts` with the following content:

If you get a type error on resolving the Product Review Module, run the Medusa application once with the `npm run dev` or `yarn dev` command to generate the necessary type definitions, as explained in the [Automatically Generated Types guide](https://docs.medusajs.com/docs/learn/fundamentals/generated-types/index.html.md).

```ts title="src/workflows/steps/create-review.ts" highlights={createReviewHighlights}
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { PRODUCT_REVIEW_MODULE } from "../../modules/product-review"
import ProductReviewModuleService from "../../modules/product-review/service"

export type CreateReviewStepInput = {
  title?: string
  content: string
  rating: number
  product_id: string
  customer_id?: string
  first_name: string
  last_name: string
  status?: "pending" | "approved" | "rejected"
}

export const createReviewStep = createStep(
  "create-review",
  async (input: CreateReviewStepInput, { container }) => {
    const reviewModuleService: ProductReviewModuleService = container.resolve(
      PRODUCT_REVIEW_MODULE
    )

    const review = await reviewModuleService.createReviews(input)

    return new StepResponse(review, review.id)
  },
  async (reviewId, { container }) => {
    if (!reviewId) {
      return
    }

    const reviewModuleService: ProductReviewModuleService = container.resolve(
      PRODUCT_REVIEW_MODULE
    )

    await reviewModuleService.deleteReviews(reviewId)
  }
)
```

You create a step with `createStep` from the Workflows SDK. It accepts two parameters:

1. The step's unique name, which is `create-review`.
2. An async function that receives two parameters:
   - The step's input, which is in this case an object with the review's properties.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.

In the step function, you resolve the Review Module's service from the Medusa container using its `resolve` method, passing it the module's name as a parameter.

Then, you create the review using the `createReviews` method. As you remember, the Review Module's service extends the `MedusaService` which generates data-management methods for you.

A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters:

1. The step's output, which is the review created.
2. Data to pass to the step's compensation function.

#### Compensation Function

The compensation function undoes the actions performed in a step. Then, if an error occurs during the workflow's execution, the compensation functions of executed steps are called to roll back the changes. This mechanism ensures data consistency in your application, especially as you integrate external systems.

The compensation function accepts two parameters:

1. The data passed from the step in the second parameter of `StepResponse`, which in this case is the ID of the created review.
2. An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

In the compensation function, you resolve the Review Module's service from the Medusa container and call the `deleteReviews` method to delete the review created in the step.

### Add createReviewWorkflow

You can now create the workflow using the step provided by Medusa and your custom step.

To create the workflow, create the file `src/workflows/create-review.ts` with the following content:

```ts title="src/workflows/create-review.ts"
import { 
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { createReviewStep } from "./steps/create-review"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

type CreateReviewInput = {
  title?: string
  content: string
  rating: number
  product_id: string
  customer_id?: string
  first_name: string
  last_name: string
  status?: "pending" | "approved" | "rejected"
}

export const createReviewWorkflow = createWorkflow(
  "create-review",
  (input: CreateReviewInput) => {
    // Check product exists
    useQueryGraphStep({
      entity: "product",
      fields: ["id"],
      filters: {
        id: input.product_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    // Create the review
    const review = createReviewStep(input)

    return new WorkflowResponse({
      review,
    })
  }
)
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is an object of the review's details.

In the workflow's constructor function, you:

- use `useQueryGraphStep` to retrieve the product. By setting the `options.throwIfKeyNotFound` to `true`, the step throws an error if the product doesn't exist.
- Call the `createReviewStep` step to create the review.

`useQueryGraphStep` uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which allows you to retrieve data across modules. For example, in the above snippet you're retrieving the product, which is managed in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md), by passing `id` to the `fields` array.

A workflow must return an instance of `WorkflowResponse`. The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is an object holding the created review in this case.

In the next step, you'll learn how to execute this workflow in an API route.

***

## Step 5: Create Review API Route

Now that you have the logic to create a product review, you need to expose it so that frontend clients, such as a storefront, can use it. You do this by creating an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts. You'll create an API route at the path `/store/reviews` that executes the workflow from the previous step.

Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

### Implement API Route

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

So, to create an API route at the path `/store/reviews`, create the file `src/api/store/reviews/route.ts` with the following content:

```ts title="src/api/store/reviews/route.ts" highlights={PostStoreReviewHighlights}
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createReviewWorkflow } from "../../../workflows/create-review"

import { z } from "zod"

export const PostStoreReviewSchema = z.object({
  title: z.string().optional(),
  content: z.string(),
  rating: z.preprocess(
    (val) => {
      if (val && typeof val === "string") {
        return parseInt(val)
      }
      return val
    },
    z.number().min(1).max(5)
  ),
  product_id: z.string(),
  first_name: z.string(),
  last_name: z.string(),
})

type PostStoreReviewReq = z.infer<typeof PostStoreReviewSchema>

export const POST = async (
  req: AuthenticatedMedusaRequest<PostStoreReviewReq>,
  res: MedusaResponse
) => {
  const input = req.validatedBody

  const { result } = await createReviewWorkflow(req.scope)
    .run({
      input: {
        ...input,
        customer_id: req.auth_context?.actor_id,
      },
    })

  res.json(result)
}
```

You first define a [Zod](https://zod.dev/) schema for the request body of the API route. You'll later use this schema to enforce validation on the API route.

Then, since you export a `POST` function, you're exposing a `POST` API route at the path `/store/reviews`. The route handler function accepts two parameters:

1. A request object with details and context on the request, such as body parameters or authenticated customer details.
2. A response object to manipulate and send the response.

`AuthenticatedMedusaRequest` accepts the request body's type as a type argument.

In the route handler, you execute the `createReviewWorkflow` workflow by invoking it, passing it the Medusa container (which is stored in the `scope` property of a request object). Then, you call its `run` method, passing to the workflow the request body as input.

### Apply Validation and Authentication Middlewares

Now that you have the API route, you need to enforce validation of the request body, and require authentication to access the route. You can do this with a middleware. A middleware is a function executed when a request is sent to an API Route. It's executed before the route handler.

Learn more about middleware in the [Middlewares documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

Middlewares are created in the `src/api/middlewares.ts` file. So create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  authenticate,
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { PostStoreReviewSchema } from "./store/reviews/route"


export default defineMiddlewares({
  routes: [
    {
      method: ["POST"], 
      matcher: "/store/reviews",
      middlewares: [
        authenticate("customer", ["session", "bearer"]),
        validateAndTransformBody(PostStoreReviewSchema),
      ],
    },
  ],
})
```

To export the middlewares, you use the `defineMiddlewares` function. It accepts an object having a `routes` property, whose value is an array of middleware route objects. Each middleware route object has the following properties:

- `method`: The HTTP methods the middleware applies to, which is in this case `POST`.
- `matcher`: The path of the route the middleware applies to.
- `middlewares`: An array of middleware functions to apply to the route. In this case, you apply two middlewares:
  - `authenticate`: ensures the request is authenticated as a customer with a session or bearer token.
  - `validateAndTransformBody`: validates that the request body parameters match the Zod schema passed as a parameter.

The create product review route is now ready for use.

### Test the API Route

To test out the API route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier.

#### Retrieve Publishable API Key

All requests sent to routes starting with `/store` must have a publishable API key in their header. This ensures that the request is scoped to a specific sales channel of your storefront.

To learn more about publishable API keys, refer to the [Publishable API Key documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md).

To retrieve the publishable API key from the Medusa Admin, refer to [this user guide](https://docs.medusajs.com/user-guide/settings/developer/publishable-api-keys/index.html.md).

#### Retrieve Customer Authentication Token

As mentioned before, the API route you added requires the customer to be authenticated. So, you'll first create a customer, then retrieve their authentication token to use in the request.

Before creating the customer, retrieve a registration token using the [Retrieve Registration JWT Token API route](https://docs.medusajs.com/api/store#auth_postactor_typeauth_provider_register):

```bash
curl -X POST 'http://localhost:9000/auth/customer/emailpass/register' \
-H 'Content-Type: application/json' \
--data-raw '{
  "email": "customer@gmail.com",
  "password": "supersecret"
}'
```

Make sure to replace the email and password with the credentials you want.

Then, register the customer using the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers):

```bash
curl -X POST 'http://localhost:9000/store/customers' \
-H 'Authorization: Bearer {token}' \
-H 'Content-Type: application/json' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
--data-raw '{
  "email": "customer@gmail.com"
}'
```

Make sure to replace:

- `{token}` with the registration token you received from the previous request.
- `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin.

Also, if you changed the email in the first request, make sure to change it here as well.

The customer is now registered. Lastly, you need to retrieve its authenticated token by sending a request to the [Authenticate Customer API route](https://docs.medusajs.com/api/store#auth_postactor_typeauth_provider):

```bash
curl -X POST 'http://localhost:9000/auth/customer/emailpass' \
-H 'Content-Type: application/json' \
--data-raw '{
  "email": "customer@gmail.com",
  "password": "supersecret"
}'
```

Copy the returned token to use it in the next requests.

#### Retrieve Product ID

Before creating a review, you need the ID of a product. You can either copy one from the Medusa Admin, or send the following request:

```bash
curl 'http://localhost:9000/store/products' \
-H 'x-publishable-api-key: {your_publishable_api_key}'
```

Make sure to replace `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin.

#### Create a Review

You can now create a review for the product you chose. To do that, send the following request:

```bash
curl --location 'http://localhost:9000/store/reviews' \
--header 'x-publishable-api-key: {your_publishable_api_key}' \
--header 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data '{
    "product_id": "{product_id}",
    "title": "Really good",
    "content": "The material is nice",
    "rating": 5,
    "first_name": "John",
    "last_name": "Smith"
}'
```

Make sure to replace:

- `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin.
- `{token}` with the authentication token you retrieved from the previous request.
- `{product_id}` with the ID of the product you chose.

If the request is successful, you'll receive a response with the created review. Notice that the review is in the `pending` status. In the upcoming steps, you'll allow admin users to approve or reject reviews.

***

## Step 6: List Reviews Admin API Route

In this step, you'll create an API route that lists the reviews of a product. You'll use this route in the Medusa Admin customizations to allow admin users to view and manage product reviews.

### Create API Route

To create the API route that retrieves a paginated list of reviews, create the file `src/api/admin/reviews/route.ts` with the following content:

```ts title="src/api/admin/reviews/route.ts" highlights={GetAdminReviewsHighlights}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createFindParams } from "@medusajs/medusa/api/utils/validators"

export const GetAdminReviewsSchema = createFindParams()

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve("query")
  
  const { 
    data: reviews, 
    metadata: { count, take, skip } = {
      count: 0,
      take: 20,
      skip: 0,
    },
  } = await query.graph({
    entity: "review",
    ...req.queryConfig,
  })

  res.json({ 
    reviews,
    count,
    limit: take,
    offset: skip,
  })
}
```

You first define a `GetAdminReviewsSchema` schema that will allow clients to pass the following query parameters:

- `limit`: The number of reviews to retrieve.
- `offset`: The number of items to skip before retrieving the reviews.
- `order`: The fields to sort the reviews by in ascending or descending order.

Then, you export a `GET` function, which exposes a `GET` API Route at the path `/admin/reviews`. In the route handler you resolve [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) from the Medusa container, which allows you to retrieve data across modules.

Next, you retrieve all reviews using Query. Notice that you pass in `query.graph` the `req.queryConfig` object. This object holds the fields to retrieve and the pagination configurations.

Finally, you return the reviews with pagination fields.

### Apply Query Configurations Middleware

After adding the API route, you need to add a middleware that validates the query parameters passed to the request, and sets the default Query configurations.

Routes starting with `/admin` are protected by default. So, you don't need to add the `authenticate` middleware to enforce authentication.

In `src/api/middlewares.ts`, add a new middleware:

```ts title="src/api/middlewares.ts"
// other imports...
import { 
  validateAndTransformQuery,
} from "@medusajs/framework/http"
import { GetAdminReviewsSchema } from "./admin/reviews/route"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/admin/reviews",
      method: ["GET"],
      middlewares: [
        validateAndTransformQuery(GetAdminReviewsSchema, {
          isList: true,
          defaults: [
            "id",
            "title",
            "content",
            "rating",
            "product_id",
            "customer_id",
            "status",
            "created_at",
            "updated_at",
            "product.*",
          ],
        }),
      ],
    },
  ],
})
```

You use the `validateAndTransformQuery` middleware to enforce validation on the query parameters passed to the request. The middleware accepts two parameters:

- The Zod schema to validate the query parameters, which is the `GetAdminReviewsSchema` schema you defined earlier.
- The Query configurations, which is an object with the following properties:
  - `isList`: A boolean that indicates whether the query is a list query.
  - `defaults`: An array of fields to retrieve by default.

You'll test the API route as you customize the Medusa Admin in the next step.

You pass `product.*` in the fields to retrieve, allowing you to retrieve the product associated with each review. This is possible because you defined a link between the `Review` data model and the `Product` data model in a previous step.

***

## Step 7: Add Reviews UI Route

Now that you have an API route that retrieves reviews, you'll customize the Medusa Admin to add a new "Reviews" page by creating a [UI Route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md).

A UI route is a React component that specifies the content to be shown in a new page in the Medusa Admin dashboard. You'll create a UI route to display the list of reviews in the Medusa Admin.

Learn more about UI routes in the [UI Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md).

### Configure JS SDK

Medusa provides a [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) that you can use to send requests to the Medusa server from any client application, including your Medusa Admin customizations.

The JS SDK is installed by default in your Medusa application. To configure it, create the file `src/admin/lib/sdk.ts` with the following content:

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: "http://localhost:9000",
  debug: process.env.NODE_ENV === "development",
  auth: {
    type: "session",
  },
})
```

You create an instance of the JS SDK using the `Medusa` class from the JS SDK. You pass it an object having the following properties:

- `baseUrl`: The base URL of the Medusa server.
- `debug`: A boolean indicating whether to log debug information into the console.
- `auth`: An object specifying the authentication type. When using the JS SDK for admin customizations, you use the `session` authentication type.

### Create UI Route

You'll now create the UI Route that lists the reviews. To do this, create the file `src/admin/routes/reviews/page.tsx` with the following content:

```tsx title="src/admin/routes/reviews/page.tsx" highlights={listUIRoutesHighlight1} collapsibleLines="1-18" expandButtonLabel="Show Imports"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { 
  createDataTableColumnHelper, 
  Container, 
  DataTable, 
  useDataTable, 
  Heading, 
  StatusBadge, 
  Toaster, 
  DataTablePaginationState,
} from "@medusajs/ui"
import { useQuery } from "@tanstack/react-query"
import { useMemo, useState } from "react"
import { sdk } from "../../lib/sdk"
import { HttpTypes } from "@medusajs/framework/types"
import { Link } from "react-router-dom"

type Review = {
  id: string
  title?: string
  content: string
  rating: number
  product_id: string
  customer_id?: string
  status: "pending" | "approved" | "rejected"
  created_at: Date
  updated_at: Date
  product?: HttpTypes.AdminProduct
  customer?: HttpTypes.AdminCustomer
}


const columnHelper = createDataTableColumnHelper<Review>()

const columns = [
  columnHelper.accessor("id", {
    header: "ID",
  }),
  columnHelper.accessor("title", {
    header: "Title",
  }),
  columnHelper.accessor("rating", {
    header: "Rating", 
  }),
  columnHelper.accessor("content", {
    header: "Content",
  }),
  columnHelper.accessor("status", {
    header: "Status",
    cell: ({ row }) => {
      const color = row.original.status === "approved" ? 
        "green" : row.original.status === "rejected" 
        ? "red" : "grey"
      return (
        <StatusBadge color={color}>
          {row.original.status.charAt(0).toUpperCase() + row.original.status.slice(1)}
          </StatusBadge>
      )
    },
  }),
  columnHelper.accessor("product", {
    header: "Product",
    cell: ({ row }) => {
      return (
        <Link
          to={`/products/${row.original.product_id}`}
        >
          {row.original.product?.title}
        </Link>
      )
    },
  }),
]

// TODO add component
```

Before defining the component, you define a `Review` type, then define the columns of the table you'll show on the page.

To display the table, you'll use the [DataTable](https://docs.medusajs.com/ui/components/data-table/index.html.md) component from Medusa UI. To define the columns of the table, you use the `createDataTableColumnHelper` function from Medusa UI, which returns a `columnHelper` object. You then use the `columnHelper` object to define the table's columns.

Next, you'll add the component that renders the content of the page. Replace the `TODO` with the following:

```tsx title="src/admin/routes/reviews/page.tsx" highlights={reviewsPageHighlights}
const limit = 15

const ReviewsPage = () => {
  const [pagination, setPagination] = useState<DataTablePaginationState>({
    pageSize: limit,
    pageIndex: 0,
  })

  const offset = useMemo(() => {
    return pagination.pageIndex * limit
  }, [pagination])

  const { data, isLoading, refetch } = useQuery<{
    reviews: Review[]
    count: number
    limit: number
    offset: number
  }>({
    queryKey: ["reviews", offset, limit],
    queryFn: () => sdk.client.fetch("/admin/reviews", {
      query: {
        offset: pagination.pageIndex * pagination.pageSize,
        limit: pagination.pageSize,
        order: "-created_at",
      },
    }),
  })

  const table = useDataTable({
    columns,
    data: data?.reviews || [],
    rowCount: data?.count || 0,
    isLoading,
    pagination: {
      state: pagination,
      onPaginationChange: setPagination,
    },
    getRowId: (row) => row.id,
  })

  return (
    <Container>
      <DataTable instance={table}>
        <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
          <Heading>
            Reviews
          </Heading>
        </DataTable.Toolbar>
        <DataTable.Table />
        <DataTable.Pagination />
      </DataTable>
      <Toaster />
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Reviews",
  icon: ChatBubbleLeftRight,
})

export default ReviewsPage
```

You create a `ReviewPage` component, which holds the UI route's content. In the component, you:

- Define state variables to configure pagination.
- Use the `useQuery` hook from `@tanstack/react-query` to fetch the reviews from the API route. In the query function, you use the JS SDK to send a request to the `/admin/reviews` API route. The JS SDK has a `client.fetch` method that has a similar signature to JavaScript's [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). You can use it to send requests to custom routes.
- Use the `useDataTable` hook from Medusa UI to create a DataTable instance. You pass the columns, data, and pagination configurations to the hook.
- Render the DataTable component, passing the DataTable instance to the `instance` prop. You also render the DataTable's toolbar, table, and pagination components.

The file also exports a configuration object created with `defineRouteConfig`. You export this object to tell Medusa that you want to add the new route to the Medusa Admin's sidebar. You specify the sidebar's item and title.

### Test the UI Route

To test out the UI route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier.

You'll find a new sidebar item `Review`. Click on it to view the list of reviews. In the upcoming steps, you'll add functionality to approve or reject reviews.

![Reviews page showing list of reviews](https://res.cloudinary.com/dza7lstvk/image/upload/v1741935325/Medusa%20Resources/Screenshot_2025-03-14_at_8.54.14_AM_tfhnyu.png)

***

## Step 8: Change Review Status API Route

Next, you want to allow the admin user to approve or reject reviews. To do this, you'll create a workflow that updates a review's status, then use it in an API route that exposes the functionality.

### Update Review Step

The workflow to update a review's status will have on step that updates the review. To create the step, create the file `src/workflows/steps/update-review.ts` with the following content:

```ts title="src/workflows/steps/update-review.ts" highlights={updateReviewStepHighlights}
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { PRODUCT_REVIEW_MODULE } from "../../modules/product-review"
import ProductReviewModuleService from "../../modules/product-review/service"

export type UpdateReviewsStepInput = {
  id: string
  status: "pending" | "approved" | "rejected"
}[]

export const updateReviewsStep = createStep(
  "update-review-step",
  async (input: UpdateReviewsStepInput, { container }) => {
    const reviewModuleService: ProductReviewModuleService = container.resolve(
      PRODUCT_REVIEW_MODULE
    )

    // Get original review before update
    const originalReviews = await reviewModuleService.listReviews({
      id: input.map((review) => review.id),
    })

    const reviews = await reviewModuleService.updateReviews(input)

    return new StepResponse(reviews, originalReviews)
  },
  async (originalData, { container }) => {
    if (!originalData) {
      return
    }

    const reviewModuleService: ProductReviewModuleService = container.resolve(
      PRODUCT_REVIEW_MODULE
    )

    // Restore original review status
    await reviewModuleService.updateReviews(originalData)
  }
)
```

This step receives an array of objects, each with the ID of the review to update and its new status.

In the step function, you first retrieve the original reviews before the update. Then, you update the reviews using the `updateReviews` method of the Review Module's service.

After that, you return the updated reviews, and you pass the original reviews to the compensation function.

In the compensation function, you restore the original reviews' status if an error occurs.

### Update Review Workflow

You can now create the workflow that uses the above step to update the review. To create the workflow, create the file `src/workflows/update-review.ts` with the following content:

```ts title="src/workflows/update-review.ts"
import {
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { updateReviewsStep } from "./steps/update-review"

export type UpdateReviewInput = {
  id: string
  status: "pending" | "approved" | "rejected"
}[]

export const updateReviewWorkflow = createWorkflow(
  "update-review",
  (input: UpdateReviewInput) => {
    const reviews = updateReviewsStep(input)

    return new WorkflowResponse({
      reviews,
    })
  }
)
```

The workflow receives an array of objects, each with the ID of the review to update and its new status. It uses the `updateReviewsStep` to update the reviews, then returns the updated reviews.

### Create API Route

Next, you'll create the API route that exposes the workflow's functionality. Create the file `src/api/admin/reviews/status/route.ts` with the following content:

```ts title="src/api/admin/reviews/status/route.ts" highlights={PostAdminUpdateReviewsStatusHighlights}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { updateReviewWorkflow } from "../../../../workflows/update-review"
import { z } from "zod"

export const PostAdminUpdateReviewsStatusSchema = z.object({
  ids: z.array(z.string()),
  status: z.enum(["pending", "approved", "rejected"]),
})

export async function POST(
  req: MedusaRequest<z.infer<typeof PostAdminUpdateReviewsStatusSchema>>, 
  res: MedusaResponse
) {
  const { ids, status } = req.validatedBody

  const { result } = await updateReviewWorkflow(req.scope).run({
    input: ids.map((id) => ({
      id,
      status,
    })),
  })

  res.json(result)
}
```

You first define a Zod schema for the request body of the API route. You'll later use this schema to enforce validation on the API route. The request body must include the following parameters:

- `ids`: An array of review IDs to update.
- `status`: The new status to set for the reviews.

Then, since you export a `POST` function, you're exposing a `POST` API route at the path `/admin/reviews/status`. In the route handler you execute the `updateReviewWorkflow` workflow, passing it the data from the request body.

Finally, you return the updated reviews.

### Apply Validation Middlewares

The last step is to add the validation middleware that enforces validation the body parameters of requests sent to the API route.

In `src/api/middlewares.ts`, add a new middleware:

```ts title="src/api/middlewares.ts"
// other imports...
import { PostAdminUpdateReviewsStatusSchema } from "./admin/reviews/status/route"

export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/reviews/status",
      method: ["POST"],
      middlewares: [
        validateAndTransformBody(PostAdminUpdateReviewsStatusSchema),
      ],
    },
  ],
})
```

You use the `validateAndTransformBody` middleware to enforce validation on an incoming request's body parameters. You pass the Zod schema you defined in the API route's file to the middleware.

In the next step, you'll customize the UI route you added earlier to allow the admin user to approve or reject reviews.

***

## Step 9: Approve and Reject Reviews in UI Route

You'll now customize the UI route you added earlier to allow the admin user to approve or reject reviews. You'll add a checkbox column to the table that allows the admin user to select multiple reviews, then choose to approve or reject them.

The `DataTable` component from Medusa UI supports a command bar that is triggered by a select (or checkbox) column in the table.

Start by adding the necessary imports at the top of `src/admin/routes/reviews/page.tsx`:

```tsx title="src/admin/routes/reviews/page.tsx"
import { 
  createDataTableCommandHelper, 
  DataTableRowSelectionState, 
} from "@medusajs/ui"
```

Then, in the `columns` array, add a new select column as the first item in the array:

```tsx title="src/admin/routes/reviews/page.tsx"
const columns = [
  columnHelper.select(),
  // ...
]
```

The select column adds a checkbox to each row in the table, allowing the admin user to select multiple reviews.

Next, you need to add the commands that allow the admin user to approve or reject the selected reviews. So, add the following after the `columns` array:

```tsx title="src/admin/routes/reviews/page.tsx" highlights={commandHelperHighlights}
const commandHelper = createDataTableCommandHelper()

const useCommands = (refetch: () => void) => {
  return [
    commandHelper.command({
      label: "Approve",
      shortcut: "A",
      action: async (selection) => {
        const reviewsToApproveIds = Object.keys(selection)

        sdk.client.fetch("/admin/reviews/status", {
          method: "POST",
          body: {
            ids: reviewsToApproveIds,
            status: "approved",
          },
        }).then(() => {
          toast.success("Reviews approved")
          refetch()
        }).catch(() => {
          toast.error("Failed to approve reviews")
        })
      },
    }),
    commandHelper.command({
      label: "Reject",
      shortcut: "R",
      action: async (selection) => {
        const reviewsToRejectIds = Object.keys(selection)

        sdk.client.fetch("/admin/reviews/status", {
          method: "POST",
          body: {
            ids: reviewsToRejectIds,
            status: "rejected",
          },
        }).then(() => {
          toast.success("Reviews rejected")
          refetch()
        }).catch(() => {
          toast.error("Failed to reject reviews")
        })
      },
    }),
  ]
}
```

You first initialize the command helper using the `createDataTableCommandHelper` function from Medusa UI. Then, you create a custom hook `useCommands` that returns an array of commands created with the command helper.

You add `Approve` and `Reject` commands, and both of them send a request to the `/admin/reviews/status` API route to update the reviews' status, but each with a different status in the request body.

Next, add the following state variable in the `ReviewsPage` component:

```tsx title="src/admin/routes/reviews/page.tsx"
const [rowSelection, setRowSelection] = useState<DataTableRowSelectionState>({})
```

This state variable will hold the selected reviews in the table.

Then, call the `useCommands` hook and pass new properties to the `useDataTable` hook:

```tsx title="src/admin/routes/reviews/page.tsx"
const commands = useCommands(refetch)

const table = useDataTable({
  // ...
  commands,
  rowSelection: {
    state: rowSelection,
    onRowSelectionChange: setRowSelection,
  },
})
```

You call the `useCommands` hook and pass it the `refetch` function (returned by `useQuery`). The `refetch` function allows you to refetch the reviews after approving or rejecting them to ensure their status in the table is updated.

Then, you pass the commands and row selection configurations (from the state variables you added) to the `useDataTable` hook.

Finally, in the `return` statement, add the command bar after the pagination component:

```tsx title="src/admin/routes/reviews/page.tsx"
<DataTable.CommandBar selectedLabel={(count) => `${count} selected`} />
```

This command bar will show the actions to perform on the selected reviews.

### Test the UI Route

To test out the UI route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard and go to the Reviews page. You'll see a new column with checkboxes that allow you to select multiple reviews.

If you try selecting multiple reviews, you'll see a command bar at the bottom center of the page that allows you to approve or reject the selected reviews.

If you choose to approve or reject the reviews, the status of the selected reviews will change, and the table will update to reflect the new status.

![Checkboxes are now shown next to the items in the table, and when you click on them the command bar shows at the bottom of the page with Approve and Reject commands](https://res.cloudinary.com/dza7lstvk/image/upload/v1741937101/Medusa%20Resources/Screenshot_2025-03-14_at_9.24.29_AM_y9vhac.png)

***

## Step 10: List Reviews Store API Route

In the upcoming steps, you'll start customizing the storefront to show the reviews of a product and allow logged-in customers to add reviews.

Before doing that, you need to add an API route that retrieves the list of approved reviews. You'll later show these in the storefront.

### Add Average Rating Method in Service

On the product's page, you want to display the average rating of a product. To do this, you'll add a method that retrieves the average rating of a product's reviews in the Review Module's service.

In `src/modules/product-review/service.ts`, add the following methods to the `ProductReviewModuleService` class:

```ts title="src/modules/product-review/service.ts"
import { InjectManager, MedusaService, MedusaContext } from "@medusajs/framework/utils"
import Review from "./models/review"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@medusajs/framework/mikro-orm/knex"

class ProductReviewModuleService extends MedusaService({
  Review,
}) {
  @InjectManager() 
  async getAverageRating(
    productId: string,
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<number> { 
    const result = await sharedContext?.manager?.execute(
      `SELECT AVG(rating) as average 
       FROM review 
       WHERE product_id = '${productId}' AND status = 'approved'`
    )

    return parseFloat(parseFloat(result?.[0]?.average ?? 0).toFixed(2))
  }
}

export default ProductReviewModuleService
```

To run queries on the database in a service's method, you need to:

- Add the `InjectManager` decorator to the method.
- Pass as the last parameter a context parameter that has the `MedusaContext` decorator.

By doing the above, Medusa injects the method with a context parameter that has a `manger` property whose value is a [forked entity manager](https://mikro-orm.io/docs/identity-map#forking-entity-manager).

Then, you run a raw SQL query to calculate the average rating of the reviews for a product with the given ID. You also filter the reviews by the status `approved`.

You'll use this method next in the API route.

### Create API Route

To create the API route that lists the reviews of a product with average rating, create the file `src/api/store/products/[id]/reviews/route.ts` with the following content:

```ts title="src/api/store/products/[id]/reviews/route.ts" highlights={GetStoreReviewsHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { PRODUCT_REVIEW_MODULE } from "../../../../../modules/product-review"
import ProductReviewModuleService from "../../../../../modules/product-review/service"
import { createFindParams } from "@medusajs/medusa/api/utils/validators"

export const GetStoreReviewsSchema = createFindParams()

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const { id } = req.params

  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
  const reviewModuleService: ProductReviewModuleService = req.scope.resolve(PRODUCT_REVIEW_MODULE)

  // Get reviews for product
  const { data: reviews, metadata: {
    count,
    take,
    skip,
  } = { count: 0, take: 10, skip: 0 } } = await query.graph({
    entity: "review",
    filters: {
      product_id: id,
      status: "approved",
    },
    ...req.queryConfig,
  })

  res.json({
    reviews,
    count,
    limit: take,
    offset: skip,
    average_rating: await reviewModuleService.getAverageRating(id),
  })
}
```

You first define a `GetStoreReviewsSchema` schema that will allow clients to pass the following query parameters:

- `limit`: The number of reviews to retrieve.
- `offset`: The number of items to skip before retrieving the reviews.
- `order`: The fields to sort the reviews by in ascending or descending order.

Then, you export a `GET` function, and that exposes a `GET` API Route at the path `/store/products/[id]/reviews`. In the route handler you resolve [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) from the Medusa container, which allows you to retrieve data across modules.

Next, you retrieve the approved reviews of a product using Query. Notice that you pass in `query.graph` the `req.queryConfig` object. This object holds the fields to retrieve and the pagination configurations. You'll configure this object in a bit.

Finally, you return the reviews with pagination fields and the average rating of the product.

### Apply Query Configurations Middleware

The last step is to add a middleware that validates the query parameters passed to the request, and sets the default Query configurations.

In `src/api/middlewares.ts`, add a new middleware:

```ts title="src/api/middlewares.ts"
// other imports
import { 
  validateAndTransformQuery,
} from "@medusajs/framework/http"
import { GetStoreReviewsSchema } from "./store/products/[id]/reviews/route"

export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/store/products/:id/reviews",
      methods: ["GET"],
      middlewares: [
        validateAndTransformQuery(GetStoreReviewsSchema, {
          isList: true,
          defaults: [
            "id", 
            "rating", 
            "title", 
            "first_name", 
            "last_name", 
            "content", 
            "created_at",
          ],
        }),
      ],
    },
  ],
})
```

You apply the `validateAndTransformQuery` middleware to the `GET` API route at the path `/store/products/:id/reviews`. Similar to before, you pass to the middleware:

- The validation schema of the request's query parameters, which is the `GetStoreReviewsSchema` you created earlier.
- An object of Query configurations. It has the following properties:
  - `isList`: A boolean indicating whether the route returns a list of items. This enables the pagination configurations.
  - `defaults`: An array of fields to retrieve by default.

By adding this middleware, you allow clients to pass pagination query parameters to the API route, and set default fields to retrieve.

You'll use this API route next as you customize the Next.js Starter Storefront.

***

## Step 11: Customize Next.js Starter Storefront

In this step, you'll customize the Next.js Starter Storefront to:

- Display a product's review and average rating on its page.
- Allow authenticated customers to submit a review for a product.

### Add Product Review Types

Before implementing the customizations, you'll add a type definition for the product review which you'll re-use in the storefront.

In `src/types/global.ts`, add the following types:

```ts title="src/types/global.ts" badgeLabel="Storefront" badgeColor="blue"
export type StoreProductReview = {
  id: string
  title: string
  rating: number
  content: string
  first_name: string
  last_name: string
}
```

You define the type of a product review object and the properties it has.

### Add Functions to Fetch and Submit Reviews

Next, you'll add two functions that fetch and submit reviews using the API routes you created earlier. To send requests to the API routes, you can use Medusa's [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md).

In `src/lib/data/products.ts`, add the following functions:

```ts title="src/lib/data/products.ts" badgeLabel="Storefront" badgeColor="blue"
import { StoreProductReview } from "../../types/global"

// ...

export const getProductReviews = async ({
  productId,
  limit = 10,
  offset = 0,
}: {
  productId: string
  limit?: number
  offset?: number 
}) => {
  const headers = {
    ...(await getAuthHeaders()),
  }

  const next = {
    ...(await getCacheOptions(`product-reviews-${productId}`)),
  }

  return sdk.client.fetch<{
    reviews: StoreProductReview[]
    average_rating: number
    limit: number
    offset: number
    count: number
  }>(`/store/products/${productId}/reviews`, {
    headers,
    query: {
      limit,
      offset,
      order: "-created_at",
    },
    next,
    cache: "force-cache",
  })
}

export const addProductReview = async (input: {
  title?: string
  content: string
  first_name: string
  last_name: string
  rating: number,
  product_id: string
}) => {
  const headers = {
    ...(await getAuthHeaders()),
  }

  return sdk.client.fetch(`/store/reviews`, {
    method: "POST",
    headers,
    body: input,
    next: {
      ...(await getCacheOptions(`product-reviews-${input.product_id}`)),
    },
    cache: "no-store",
  })
}
```

You define two functions:

- `getProductReviews`: Fetches the reviews of a product with the given ID. It accepts an object with the product ID, and optional limit and offset parameters, allowing you to paginate the reviews.
- `addProductReview`: Submits a review for a product. It accepts an object with the review's details.

To send requests to your custom API routes, you use the JS SDK's `client.fetch` method.

### Add Product Review Form

You'll now create a component that shows the product review form for authenticated customers. Afterwards, you'll display this component on the product's page.

To create the form component, create the file `src/modules/products/components/product-reviews/form.tsx` with the following content:

```tsx title="src/modules/products/components/product-reviews/form.tsx" badgeLabel="Storefront" badgeColor="blue"
"use client"

import { useState } from "react"

import { useEffect } from "react"
import { retrieveCustomer } from "../../../../lib/data/customer"
import { HttpTypes } from "@medusajs/types"
import { Button, Input, Label, Textarea, toast, Toaster } from "@medusajs/ui"
import { Star, StarSolid } from "@medusajs/icons"
import { addProductReview } from "../../../../lib/data/products"

type ProductReviewsFormProps = {
  productId: string
}

export default function ProductReviewsForm({ productId }: ProductReviewsFormProps) {
  const [customer, setCustomer] = useState<HttpTypes.StoreCustomer | null>(null)
  const [isLoading, setIsLoading] = useState(false)
  const [showForm, setShowForm] = useState(false)
  const [title, setTitle] = useState("")
  const [content, setContent] = useState("")
  const [rating, setRating] = useState(0)

  useEffect(() => {
    if (customer) {
      return
    }

    retrieveCustomer().then(setCustomer)
  }, [])

  if (!customer) {
    return <></>
  }

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    if (!content || !rating) {
      toast.error("Error", {
        description: "Please fill in required fields.",
      })
      return
    }

    e.preventDefault()
    setIsLoading(true)
    addProductReview({
      title,
      content,
      rating,
      first_name: customer.first_name || "",
      last_name: customer.last_name || "",
      product_id: productId,
    }).then(() => {
      setShowForm(false)
      setTitle("")
      setContent("")
      setRating(0)
      toast.success("Success", {
        description: "Your review has been submitted and is awaiting approval.",
      })
    }).catch(() => {
      toast.error("Error", {
        description: "An error occurred while submitting your review. Please try again later.",
      })
    }).finally(() => {
      setIsLoading(false)
    })
  }

  // TODO render form
}
```

You create a `ProductReviewsForm` component that accepts the product's ID as a prop. In the component, you:

- Fetch the authenticated customer's details. If the customer is not authenticated, you return an empty fragment.
- Implement a `handleSubmit` function that submits the review when the form is submitted.

Next, you'll add a return statement that shows the form when the customer is authenticated. Replace the `TODO` with the following:

```tsx title="src/modules/products/components/product-reviews/form.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div className="product-page-constraint mt-8">
    {!showForm && (
      <div className="flex justify-center">
        <Button variant="secondary" onClick={() => setShowForm(true)}>Add a review</Button>
      </div>
    )}
    {showForm && (
      <div className="flex flex-col gap-y-4">
        <div className="flex flex-col gap-y-2">
          <span className="text-xl-regular text-ui-fg-base">
          Add a review
        </span>
        
        <form onSubmit={handleSubmit} className="flex flex-col gap-y-4">
          <div className="flex flex-col gap-y-2">
            <Label>Title</Label>
            <Input name="title" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Title" />
          </div>
          <div className="flex flex-col gap-y-2">
            <Label>Content</Label>
            <Textarea name="content" value={content} onChange={(e) => setContent(e.target.value)} placeholder="Content" />
          </div>
          <div className="flex flex-col gap-y-2">
            <Label>Rating</Label>
            <div className="flex gap-x-1">
              {Array.from({ length: 5 }).map((_, index) => (
                <Button key={index} variant="transparent" onClick={(e) => {
                  e.preventDefault()
                  setRating(index + 1)
                }} className="p-0">
                  {rating >= index + 1 ? <StarSolid className="text-ui-tag-orange-icon" /> : <Star />}
                </Button>
              ))}
            </div>
          </div>
          <Button type="submit" disabled={isLoading} variant="primary">Submit</Button>
        </form>
        </div>
      </div>
    )}
    <Toaster />
  </div>
)
```

In the return statement, you:

- Show an "Add a review" button. When clicked, the form is displayed.
- In the form, you show the customer fields for the title, content, and rating for the review. The rating input is displayed as stars, and the customer can click on a star to set the rating.
- When the form is submitted, you call the `handleSubmit` function to submit the review.

### Display Product Reviews

Now, you'll add the components to display the product reviews and the product review form on the product's page.

Create the file `src/modules/products/components/product-reviews/index.tsx` with the following content:

```tsx title="src/modules/products/components/product-reviews/index.tsx" badgeLabel="Storefront" badgeColor="blue"
"use client"

import { getProductReviews } from "../../../../lib/data/products"
import { Star, StarSolid } from "@medusajs/icons"
import { StoreProductReview } from "../../../../types/global"
import { Button } from "@medusajs/ui"
import { useState, useEffect } from "react"
import ProductReviewsForm from "./form"
type ProductReviewsProps = {
  productId: string
}

export default function ProductReviews({
  productId,
}: ProductReviewsProps) {
  const [page, setPage] = useState(1)
  const defaultLimit = 10
  const [reviews, setReviews] = useState<StoreProductReview[]>([])
  const [rating, setRating] = useState(0)
  const [hasMoreReviews, setHasMoreReviews] = useState(false)
  const [count, setCount] = useState(0)

  useEffect(() => {
    getProductReviews({
      productId,
      limit: defaultLimit,
      offset: (page - 1) * defaultLimit,
    }).then(({ reviews: paginatedReviews, average_rating, count, limit }) => {
      setReviews((prev) => {
        const newReviews = paginatedReviews.filter(
          (review) => !prev.some((r) => r.id === review.id)
        )
        return [...prev, ...newReviews]
      })
      setRating(Math.round(average_rating))
      console.log(count, limit, page, count > limit * page)
      setHasMoreReviews(count > limit * page)
      setCount(count)
    })
  }, [page])

  // TODO add return statement
}
```

You create a `ProductReviews` component that accepts the product's ID as a prop. In the component, you:

- Define state variables related to the reviews and pagination.
- When the page changes, you fetch the reviews of the product with the given ID.

Before adding the return statement that will show the reviews and the create-review form, you'll add a component that renders a single review.

Add the following component after the `ProductReviews` component:

```tsx title="src/modules/products/components/product-reviews/index.tsx" badgeLabel="Storefront" badgeColor="blue"
function Review({ review }: { review: StoreProductReview }) {
  return (
    <div className="flex flex-col gap-y-2 text-base-regular text-ui-fg-base">
      <div className="flex gap-x-2 items-center">
        {review.title && <strong>{review.title}</strong>}
        <div className="flex gap-x-1">
          {Array.from({ length: 5 }).map((_, index) => (
            <span key={index}>
              {index <= review.rating ? (
                <StarSolid className="text-ui-tag-orange-icon" />
              ) : (
                <Star />
              )}
            </span>
          ))}
        </div>
      </div>
      <div>{review.content}</div>
      <div className="border-t border-ui-border-base pt-4 text-sm-regular">
        {review.first_name} {review.last_name}
      </div>
    </div>
  )
}
```

You add a `Review` component that accepts a review object as a prop. In the component, you render the review's title, rating, content, and the reviewer's name.

Next, replace the `TODO` in the `ProductReviews` component with the following:

```tsx title="src/modules/products/components/product-reviews/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div className="product-page-constraint">
    <div className="flex flex-col items-center text-center mb-16">
      <span className="text-base-regular text-gray-600 mb-6">
        Product Reviews
      </span>
      <p className="text-2xl-regular text-ui-fg-base max-w-lg">
        See what our customers are saying about this product.
      </p>
      <div className="flex gap-x-2 justify-center items-center">
        <div className="flex gap-x-2">
          {Array.from({ length: 5 }).map((_, index) => (
            <span key={index}>
              {!rating || index > rating ? (
                <Star />
              ) : (
                <StarSolid className="text-ui-tag-orange-icon" />
              )}
            </span>
          ))}
        </div>
        <span className="text-base-regular text-gray-600">
          {count} reviews
        </span>
      </div>
    </div>

    <div className="grid grid-cols-1 small:grid-cols-2 gap-x-6 gap-y-8">
      {reviews.map((review) => (
        <Review key={review.id} review={review} />
      ))}
    </div>

    {hasMoreReviews && (
      <div className="flex justify-center mt-8">
        <Button variant="secondary" onClick={() => setPage(page + 1)}>
          Load more reviews
        </Button>
      </div>
    )}

    <ProductReviewsForm productId={productId} />
  </div>
)
```

You show the average rating of the product and the number of reviews. Then, you show every review loaded. You also show a "Load more reviews" button if there are more reviews to load, which changes the `page` and fetches more reviews.

After the reviews, you show the `ProductReviewsForm` component to allow authenticated customers to submit a review.

### Display Product Reviews on Product Page

Finally, you'll customize the product's page to show the `ProductReviews` component.

In `src/modules/products/templates/index.tsx`, import the `ProductReviews` component at the top of the file:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import ProductReviews from "../components/product-reviews"
```

Then, add the `ProductReviews` component before the `div` wrapping the `RelatedProducts` component:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
<div className="content-container my-16 small:my-32">
  <ProductReviews productId={product.id} />
</div>
```

This will show the product reviews after the product's image and details, but before the related products.

### Test the Customizations

To test out both the server and storefront customizations, first, start the Medusa application by running the following command in its directory:

```bash npm2yarn
npm run dev
```

Then, start the Next.js Starter Storefront by running the following command in its directory:

```bash npm2yarn
npm run dev
```

The storefront will run at `http://localhost:8000`. Open it, then click on Menu -> Store. This will show you the list of products.

If you click on one of them and scroll down below the images, you'll find a section showing the average rating and reviews of the product.

![Product page showing the average rating and reviews of the product](https://res.cloudinary.com/dza7lstvk/image/upload/v1741937534/Medusa%20Resources/Screenshot_2025-03-14_at_9.31.58_AM_tw9vui.png)

To add a review, you first need to log in as a customer. You can do so by clicking on Account at the top right of the page. In the new page, either enter the credentials of the customer you created earlier, or create a new customer.

Afterwards, go back to the product's page, you'll see the "Add a review" button below the reviews.

![Product page showing the Add a review button](https://res.cloudinary.com/dza7lstvk/image/upload/v1741937731/Medusa%20Resources/Screenshot_2025-03-14_at_9.35.11_AM_w1wzdp.png)

If you click on the button, a form will appear where you can fill in the review's details and submit it.

![Product page showing the Add a review form](https://res.cloudinary.com/dza7lstvk/image/upload/v1741938961/Medusa%20Resources/Screenshot_2025-03-14_at_9.55.37_AM_epnfz0.png)

After submitting the review, you can approve or reject it from the Medusa Admin dashboard.

***

## Next Steps

You've now implemented product-review features in Medusa. There's still more that you can implement to enhance these features:

- Link a Review to a customer as you did in [Step 3](#step-3-define-review--product-link) and customize the storefront to show the customer's reviews on their profile.
- Add a feature to allow customers to upvote or downvote reviews.
- Allow customers to add images to their reviews.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Implement Quick Re-Order Functionality in Medusa

In this tutorial, you'll learn how to implement a re-order functionality in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) which are available out-of-the-box. The features include order-management features.

The Medusa Framework facilitates building custom features that are necessary for your business use case. In this tutorial, you'll learn how to implement a re-order functionality in Medusa. This feature is useful for businesses whose customers are likely to repeat their orders, such as B2B or food delivery businesses.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer.

## Summary

By following this tutorial, you'll learn how to:

- Install and set up Medusa.
- Define the logic to re-order an order.
- Customize the Next.js Starter Storefront to add a re-order button.

![Diagram showcasing the re-order logic](https://res.cloudinary.com/dza7lstvk/image/upload/v1746451309/Medusa%20Resources/reorder-summary_wnbbws.jpg)

- [Re-Order Repository](https://github.com/medusajs/examples/tree/main/re-order): Find the full code for this guide in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1741941475/OpenApi/product-reviews_jh8ohj.yaml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Implement Re-Order Workflow

To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task.

By using workflows, you can track their executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an API Route.

In this section, you'll implement the re-order functionality in a workflow. Later, you'll execute the workflow in a custom API route.

Refer to the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) to learn more.

The workflow will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the order's details.
- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md): Create a cart for the re-order.
- [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md): Add the order's shipping method(s) to the cart.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details.

This workflow uses steps from Medusa's `@medusajs/medusa/core-flows` package. So, you can implement the workflow without implementing custom steps.

### a. Create the Workflow

To create the workflow, create the file `src/workflows/reorder.ts` with the following content:

```ts title="src/workflows/reorder.ts" highlights={workflowHighlights1}
import { 
  createWorkflow, 
  transform, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  addShippingMethodToCartWorkflow, 
  createCartWorkflow, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"

type ReorderWorkflowInput = {
  order_id: string
}

export const reorderWorkflow = createWorkflow(
  "reorder",
  ({ order_id }: ReorderWorkflowInput) => {
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "*",
        "items.*",
        "shipping_address.*",
        "billing_address.*",
        "region.*",
        "sales_channel.*",
        "shipping_methods.*",
        "customer.*",
      ],
      filters: {
        id: order_id,
      },
    })

    // TODO create a cart with the order's items
  }
)
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is an object holding the ID of the order to re-order.

In the workflow's constructor function, so far you use the `useQueryGraphStep` step to retrieve the order's details. This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) under the hood, which allows you to query data across [modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).

Refer to the [Query documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to learn more about how to use it.

### b. Create a Cart

Next, you need to create a cart using the old order's details. You can use the `createCartWorkflow` step to create a cart, but you first need to prepare its input data.

Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/reorder.ts" highlights={workflowHighlights2}
const createInput = transform({
  orders,
}, (data) => {
  return {
    region_id: data.orders[0].region_id!,
    sales_channel_id: data.orders[0].sales_channel_id!,
    customer_id: data.orders[0].customer_id!,
    email: data.orders[0].email!,
    billing_address: {
      first_name: data.orders[0].billing_address?.first_name!,
      last_name: data.orders[0].billing_address?.last_name!,
      address_1: data.orders[0].billing_address?.address_1!,
      city: data.orders[0].billing_address?.city!,
      country_code: data.orders[0].billing_address?.country_code!,
      province: data.orders[0].billing_address?.province!,
      postal_code: data.orders[0].billing_address?.postal_code!,
      phone: data.orders[0].billing_address?.phone!,
    },
    shipping_address: {
      first_name: data.orders[0].shipping_address?.first_name!,
      last_name: data.orders[0].shipping_address?.last_name!,
      address_1: data.orders[0].shipping_address?.address_1!,
      city: data.orders[0].shipping_address?.city!,
      country_code: data.orders[0].shipping_address?.country_code!,
      province: data.orders[0].shipping_address?.province!,
      postal_code: data.orders[0].shipping_address?.postal_code!,
      phone: data.orders[0].shipping_address?.phone!,
    },
    items: data.orders[0].items?.map((item) => ({
      variant_id: item?.variant_id!,
      quantity: item?.quantity!,
      unit_price: item?.unit_price!,
    })),
  }
})

const { id: cart_id } = createCartWorkflow.runAsStep({
  input: createInput,
})

// TODO add the shipping method to the cart
```

Data manipulation is not allowed in a workflow, as Medusa stores its definition before executing it. Instead, you can use `transform` from the Workflows SDK to manipulate the data.

Learn more about why you can't manipulate data in a workflow and the `transform` function in the [Data Manipulation in Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md).

`transform` accepts the following parameters:

1. The data to use in the transformation function.
2. A transformation function that accepts the data from the first parameter and returns the transformed data.

In the above code snippet, you use `transform` to create the input for the `createCartWorkflow` step. The input is an object that holds the cart's details, including its items, shipping and billing addresses, and more.

Learn about other input parameters you can pass in the [createCartWorkflow reference](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md).

After that, you execute the `createCartWorkflow` passing it the transformed input. The workflow returns the cart's details, including its ID.

### c. Add Shipping Methods

Next, you need to add the order's shipping method(s) to the cart. This saves the customer from having to select a shipping method again.

You can use the `addShippingMethodToCartWorkflow` step to add the shipping method(s) to the cart.

Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/reorder.ts" highlights={workflowHighlights3}
const addShippingMethodToCartInput = transform({
  cart_id,
  orders,
}, (data) => {
  return {
    cart_id: data.cart_id,
    options: data.orders[0].shipping_methods?.map((method) => ({
      id: method?.shipping_option_id!,
      data: method?.data!,
    })) ?? [],
  }
})

addShippingMethodToCartWorkflow.runAsStep({
  input: addShippingMethodToCartInput,
})

// TODO retrieve and return the cart's details
```

Again, you use `transform` to prepare the input for the `addShippingMethodToCartWorkflow`. The input includes the cart's ID and the shipping method(s) to add to the cart.

Then, you execute the `addShippingMethodToCartWorkflow` to add the shipping method(s) to the cart.

### d. Retrieve and Return the Cart's Details

Finally, you need to retrieve the cart's details and return them as the workflow's output.

Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/reorder.ts" highlights={workflowHighlights4}
const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: [
    "*",
    "items.*",
    "shipping_methods.*",
    "shipping_address.*",
    "billing_address.*",
    "region.*",
    "sales_channel.*",
    "promotions.*",
    "currency_code",
    "subtotal",
    "item_total",
    "total",
    "item_subtotal",
    "shipping_subtotal",
    "customer.*",
    "payment_collection.*",
    
  ],
  filters: {
    id: cart_id,
  },
}).config({ name: "retrieve-cart" })

return new WorkflowResponse(carts[0])
```

You execute the `useQueryGraphStep` again to retrieve the cart's details. Since you're re-using a step, you have to rename it using the `config` method.

Finally, you return the cart's details. A workflow must return an instance of `WorkflowResponse`.

The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is the cart's details in this case.

In the next step, you'll create an API route that exposes the re-order functionality.

***

## Step 3: Create Re-Order API Route

Now that you have the logic to re-order, you need to expose it so that frontend clients, such as a storefront, can use it. You do this by creating an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts. You'll create an API route at the path `/store/customers/me/orders/:id` that executes the workflow from the previous step.

Refer to the [API Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) to learn more.

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

So, create the file `src/api/store/customers/me/orders/[id]/route.ts` with the following content:

```ts title="src/api/store/customers/me/orders/[id]/route.ts"
import { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { reorderWorkflow } from "../../../../../../workflows/reorder"

export async function POST(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  const { id } = req.params

  const { result } = await reorderWorkflow(req.scope).run({
    input: {
      order_id: id,
    },
  })

  return res.json({
    cart: result,
  })
}
```

Since you export a `POST` route handler function, you expose a `POST` API route at `/store/customers/me/orders/:id`.

API routes that start with `/store/customers/me` are protected by default, meaning that only authenticated customers can access them. Learn more in the [Protected API Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md).

The route handler function accepts two parameters:

1. A request object with details and context on the request, such as path parameters or authenticated customer details.
2. A response object to manipulate and send the response.

In the route handler function, you execute the `reorderWorkflow`. To execute a workflow, you:

- Invoke it, passing it the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) available in the `req.scope` property.
  - The Medusa container is a registry of Framework and commerce resources that you can resolve and use in your customizations.
- Call the `run` method, passing it an object with the workflow's input.

You pass the order ID from the request's path parameters as the workflow's input. Finally, you return the created cart's details in the response.

You'll test out this API route after you customize the Next.js Starter Storefront.

***

## Step 4: Customize the Next.js Starter Storefront

In this step, you'll customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to add a re-order button. You installed the Next.js Starter Storefront in the first step with the Medusa application, but you can also install it separately as explained in the [Next.js Starter Storefront documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).

The Next.js Starter Storefront provides rich commerce features and a sleek design. You can use it as-is or build on top of it to tailor it for your business's unique use case, design, and customer experience.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-reorder`, you can find the storefront by going back to the parent directory and changing to the `medusa-reorder-storefront` directory:

```bash
cd ../medusa-reorder-storefront # change based on your project name
```

To add the re-order button, you will:

- Add a server function that re-orders an order using the API route from the previous step.
- Add a button to the order details page that calls the server function.

### a. Add the Server Function

You'll add the server function for the re-order functionality in the `src/lib/data/orders.ts` file.

First, add the following import statement to the top of the file:

```ts title="src/lib/data/orders.ts" badgeLabel="Storefront" badgeColor="blue"
import { setCartId } from "./cookies"
```

Then, add the function at the end of the file:

```ts title="src/lib/data/orders.ts" badgeLabel="Storefront" badgeColor="blue"
export const reorder = async (id: string) => {
  const headers = await getAuthHeaders()

  const { cart } = await sdk.client.fetch<HttpTypes.StoreCartResponse>(
    `/store/customers/me/orders/${id}`,
    {
      method: "POST",
      headers,
    }
  )

  await setCartId(cart.id)

  return cart
}
```

You add a function that accepts the order ID as a parameter.

The function uses the `client.fetch` method of the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) to send a request to the API route you created in the previous step.

The JS SDK is already configured in the Next.js Starter Storefront. Refer to the [JS SDK documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) to learn more about it.

Once the request succeeds, you use the `setCartId` function that's defined in the storefront to set the cart ID in a cookie. This ensures the cart is used across the storefront.

Finally, you return the cart's details.

### b. Add the Re-Order Button Component

Next, you'll add the component that shows the re-order button. You'll later add the component to the order details page.

To create the component, create the file `src/modules/order/components/reorder-action/index.tsx` with the following content:

```tsx title="src/modules/order/components/reorder-action/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={componentHighlights}
import { Button, toast } from "@medusajs/ui"
import { reorder } from "../../../../lib/data/orders"
import { useState } from "react"
import { useRouter } from "next/navigation"

type ReorderActionProps = {
  orderId: string
}

export default function ReorderAction({ orderId }: ReorderActionProps) {
  const [isLoading, setIsLoading] = useState(false)
  const router = useRouter()

  const handleReorder = async () => {
    setIsLoading(true)
    try {
      const cart = await reorder(orderId)

      setIsLoading(false)
      toast.success("Prepared cart to reorder. Proceeding to checkout...")
      router.push(`/${cart.shipping_address!.country_code}/checkout?step=payment`)
    } catch (error) {
      setIsLoading(false)
      toast.error(`Error reordering: ${error}`)
    }
  }

  return (
    <Button
      variant="primary"
      size="small"
      onClick={handleReorder}
      disabled={isLoading}
    >
      Reorder
    </Button>
  )
}
```

You create a `ReorderAction` component that accepts the order ID as a prop.

In the component, you render a button that, when clicked, calls a `handleReorder` function. The function calls the `reorder` function you created in the previous step to re-order the order.

If the re-order succeeds, you redirect the user to the payment step of the checkout page. If it fails, you show an error message.

### c. Show Re-Order Button on Order Details Page

Finally, you'll show the `ReorderAction` component on the order details page.

In `src/modules/order/templates/order-details-template.tsx`, add the following import statement to the top of the file:

```tsx title="src/modules/order/templates/order-details-template.tsx" badgeLabel="Storefront" badgeColor="blue"
import ReorderAction from "../components/reorder-action"
```

Then, in the return statement of the `OrderDetailsTemplate` component, find the `OrderDetails` component and add the `ReorderAction` component below it:

```tsx title="src/modules/order/templates/order-details-template.tsx" badgeLabel="Storefront" badgeColor="blue"
<ReorderAction orderId={order.id} />
```

The re-order button will now be shown on the order details page.

### Test it Out

You'll now test out the re-order functionality.

First, to start the Medusa application, run the following command in the Medusa application's directory:

```bash npm2yarn badgeLabel="Medusa application" badgeColor="green"
npm run dev
```

Then, in the Next.js Starter Storefront directory, run the following command to start the storefront:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

The storefront will be running at `http://localhost:8000`. Open it in your browser.

To test out the re-order functionality:

- Create an account in the storefront.
- Add a product to the cart and complete the checkout process to place an order.
- Go to Account -> Orders, and click on the "See details" button.

![Orders page on customer account](https://res.cloudinary.com/dza7lstvk/image/upload/v1746449666/Medusa%20Resources/Screenshot_2025-05-05_at_3.52.46_PM_ae4e78.png)

On the order's details page, you'll find a "Reorder" button.

![Reorder button on order details page](https://res.cloudinary.com/dza7lstvk/image/upload/v1746450255/Medusa%20Resources/Screenshot_2025-05-05_at_4.03.51_PM_xaslqo.png)

When you click on the button, a new cart will be created with the order's details, and you'll be redirected to the checkout page where you can complete the purchase.

![Checkout page showing the payment step](https://res.cloudinary.com/dza7lstvk/image/upload/v1746450342/Medusa%20Resources/Screenshot_2025-05-05_at_4.05.29_PM_vqpdqo.png)

***

## Next Steps

You now have a re-order functionality in your Medusa application and Next.js Starter Storefront. You can expand more on this feature based on your use case.

For example, you can add quick orders on the storefront's homepage, allowing customers to quickly re-order their last orders.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Use Saved Payment Methods During Checkout

In this tutorial, you'll learn how to allow customers to save their payment methods and use them for future purchases.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) which are available out-of-the-box.

Medusa's architecture facilitates integrating third-party services, such as payment providers. These payment providers can process payments and securely store customers' payment methods for future use.

In this tutorial, you'll expand on Medusa's [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) to allow customers to re-use their saved payment methods during checkout.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer.

While this tutorial uses Stripe as an example, you can follow the same steps to implement saved payment methods with other payment providers.

## Summary

By following this tutorial, you'll learn how to:

- Install and set up Medusa and the Next.js Starter Storefront.
- Set up the Stripe Module Provider in Medusa.
- Customize the checkout flow to save customers' payment methods.
- Allow customers to select saved payment methods during checkout.

![Diagram illustrating the features of this guide](https://res.cloudinary.com/dza7lstvk/image/upload/v1745309355/Medusa%20Resources/saved-payment-methods_orjnix.jpg)

[Saved Payment Methods Repository](https://github.com/medusajs/examples/tree/main/stripe-saved-payment): Find the full code for this guide in this repository.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Set Up the Stripe Module Provider

Medusa's [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md) provides payment-related models and the interface to manage and process payments. However, it delegates the actual payment processing to module providers that integrate third-party payment services.

The [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) is a Payment Module Provider that integrates Stripe into your Medusa application to process payments. It can also save payment methods securely.

In this section, you'll set up the Stripe Module Provider in your Medusa application.

### Prerequisites

- [Stripe account](https://stripe.com/)
- [Stripe Secret and Public API Keys](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard)

### Register the Stripe Module Provider

To register the Stripe Module Provider in your Medusa application, add it to the array of providers passed to the Payment Module in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/payment",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/payment-stripe",
            id: "stripe",
            options: {
              apiKey: process.env.STRIPE_API_KEY,
            },
          },
        ],
      },
    },
  ],
})
```

The Medusa configuration accepts a `modules` array, which contains the modules to be loaded. While the Payment Module is loaded by default, you need to add it again when registering a new provider.

You register provides in the `providers` option of the Payment Module. Each provider is an object with the following properties:

- `resolve`: The package name of the provider.
- `id`: The ID of the provider. This is used to identify the provider in the Medusa application.
- `options`: The options to be passed to the provider. In this case, the `apiKey` option is required for the Stripe Module Provider.

Learn about other options in the [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe#module-options/index.html.md) documentation.

### Add Environment Variables

Next, add the following environment variables to your `.env` file:

```plain
STRIPE_API_KEY=sk_...
```

Where `STRIPE_API_KEY` is your Stripe Secret API Key. You can find it in the Stripe dashboard under Developers > API keys.

![Secret API Key in the Stripe dashboard](https://res.cloudinary.com/dza7lstvk/image/upload/v1745313823/Medusa%20Resources/Screenshot_2025-04-22_at_12.20.08_PM_w6rxxo.png)

### Enable Stripe in a Region

In Medusa, each [region](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md) (which is a geographical area where your store operates) can have different payment methods enabled. So, after registering the Stripe Module Provider, you need to enable it in a region.

To enable it in a region, start the Medusa application with the following command:

```bash npm2yarn
npm run dev
```

Then, go to `localhost:9000/app` and log in with the user you created earlier.

Once you're logged in:

1. Go to Settings -> Regions.
2. Click on the region where you want to enable the payment provider.
3. Click the <InlineIcon Icon={EllipsisHorizontal} alt="three-dots" /> icon at the top right of the first section
4. Choose "Edit" from the dropdown menu
5. In the side window that opens, find the "Payment Providers" field and select Stripe from the dropdown.
6. Once you're done, click the "Save" button.

Stripe will now be available as a payment option during checkout.

The Stripe Module Provider supports different payment methods in Stripe, such as Bancontact or iDEAL. This guide focuses only on the card payment method, but you can enable other payment methods as well.

![Stripe in the dropdown](https://res.cloudinary.com/dza7lstvk/image/upload/v1745245433/Medusa%20Resources/Screenshot_2025-04-21_at_5.23.07_PM_yd7xji.png)

### Add Evnironement Variable to Storefront

The [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) supports payment with Stripe during checkout if it's enabled in the region.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-payment`, you can find the storefront by going back to the parent directory and changing to the `medusa-payment-storefront` directory:

```bash
cd ../medusa-payment-storefront # change based on your project name
```

In the Next.js Starter Storefront project, add the Stripe public API key as an environment variable in `.env.local`:

```plain badgeLabel="Storefront" badgeColor="blue"
NEXT_PUBLIC_STRIPE_KEY=pk_123...
```

Where `NEXT_PUBLIC_STRIPE_KEY` is your Stripe public API key. You can find it in the Stripe dashboard under Developers > API keys.

***

## Step 3: List Payment Methods API Route

The Payment Module uses [account holders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/account-holder/index.html.md) to represent a customer's details that are stored in a third-party payment provider. Medusa creates an account holder for each customer, allowing you later to retrieve the customer's saved payment methods in the third-party provider.

![Diagram illustrating the relation between customers and account holders in Medusa, and customers in Stripe](https://res.cloudinary.com/dza7lstvk/image/upload/v1745314344/Medusa%20Resources/customer-account-stripe_in5ei6.jpg)

While this feature is available out-of-the-box, you need to expose it to clients, like storefronts, by creating an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). An API Route is an endpoint that exposes commerce features to external applications and clients.

In this step, you'll create an API route that lists the saved payment methods for an authenticated customer.

Refer to the [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) documentation to learn more.

### Create API Route

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`, and it can include path parameters using square brackets.

So, to create an API route at the path `/store/payment-methods/:account-holder-id`, create the file `src/api/store/payment-methods/[account_holder_id]/route.ts` with the following content:

```ts title="src/api/store/payment-methods/[account_holder_id]/route.ts" highlights={apiRouteHighlights}
import { MedusaError } from "@medusajs/framework/utils"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { account_holder_id } = req.params
  const query = req.scope.resolve("query")
  const paymentModuleService = req.scope.resolve("payment")

  const { data: [accountHolder] } = await query.graph({
    entity: "account_holder",
    fields: [
      "data",
      "provider_id",
    ],
    filters: {
      id: account_holder_id,
    },
  })

  if (!accountHolder) {
    throw new MedusaError(
      MedusaError.Types.NOT_FOUND, 
      "Account holder not found"
    )
  }

  const paymentMethods = await paymentModuleService.listPaymentMethods(
    {
      provider_id: accountHolder.provider_id,
      context: {
        account_holder: {
          data: {
            id: accountHolder.data.id,
          },
        },
      },
    }
  )

  res.json({
    payment_methods: paymentMethods,
  })
}
```

Since you export a route handler function named `GET`, you expose a `GET` API route at `/store/payment-methods/:account-holder-id`. The route handler function accepts two parameters:

1. A request object with details and context on the request, such as body parameters or authenticated customer details.
2. A response object to manipulate and send the response.

The request object has a `scope` property, which is an instance of the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). The Medusa container is a registry of Framework and commerce tools that you can access in the API route.

You use the Medusa container to resolve:

- [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which is a tool that retrieves data across modules in the Medusa application.
- The [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md)'s service, which provides an interface to manage and process payments with third-party providers.

You use Query to retrieve the account holder with the ID passed as a path parameter. If the account holder is not found, you throw an error.

Then, you use the [listPaymentMethods](https://docs.medusajs.com/references/payment/listPaymentMethods/index.html.md) method of the Payment Module's service to retrieve the payment providers saved in the third-party provider. The method accepts an object with the following properties:

- `provider_id`: The ID of the provider, such as Stripe's ID. The account holder stores the ID its associated provider.
- `context`: The context of the request. In this case, you pass the account holder's ID to retrieve the payment methods associated with it in the third-party provider.

Finally, you return the payment methods in the response.

### Protect API Route

Only authenticated customers can access and use saved payment methods. So, you need to protect the API route to ensure that only authenticated customers can access it.

To protect an API route, you can add a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md). A middleware is a function executed when a request is sent to an API Route. You can add an authentication middleware that ensures that the request is authenticated before executing the route handler function.

Refer to the [Middlewares](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) documentation to learn more.

Middlewares are added in the `src/api/middlewares.ts` file. So, create the file with the following content:

```ts title="src/api/middlewares.ts"
import { authenticate, defineMiddlewares } from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/store/payment-methods/:provider_id/:account_holder_id",
      method: "GET",
      middlewares: [
        authenticate("customer", ["bearer", "session"]),
      ],
    },
  ],
})
```

The `src/api/middlewares.ts` file must use the `defineMiddlewares` function and export its result. The `defineMiddlewares` function accepts a `routes` array that accepts objects with the following properties:

- `matcher`: The path of the API route to apply the middleware to.
- `method`: The HTTP method of the API route to apply the middleware to.
- `middlewares`: An array of middlewares to apply to the API route.

You apply the `authenticate` middleware to the API route you created earlier. The `authenticate` middleware ensures that only authenticated customers can access the API route.

Refer to the [Protected Routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md) documentation to learn more about the `authenticate` middleware.

Your API route can now only be accessed by authenticated customers. You'll test it out as you customize the Next.js Starter Storefront in the next steps.

***

## Step 4: Save Payment Methods During Checkout

In this step, you'll customize the checkout flow in the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to save payment methods during checkout.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-payment`, you can find the storefront by going back to the parent directory and changing to the `medusa-payment-storefront` directory:

```bash
cd ../medusa-payment-storefront # change based on your project name
```

During checkout, when the customer chooses a payment method, such as Stripe, the Next.js Starter Storefront creates a [payment session](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-session/index.html.md) in Medusa using the [Initialize Payment Session](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions) API route.

Under the hood, Medusa uses the associated payment provider (Stripe) to initiate the payment process with the associated third-party payment provider. The [Initialize Payment Session](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions) API route accepts a `data` object parameter in the request body that allows you to pass data relevant to the third-party payment provider.

So, to save the payment method that the customer uses during checkout with Stripe, you must pass the `setup_future_usage` property in the `data` object. The `setup_future_usage` property is a Stripe-specific property that allows you to save the payment method for future use.

In `src/modules/checkout/components/payment/index.tsx` of the Next.js Starter Storefront, there are two uses of the `initiatePaymentSession` function. Update each of them to pass the `data` property:

```ts title="src/modules/checkout/components/payment/index.tsx" badgeLabel="Storefront" badgeColor="blue"
// update in two places
await initiatePaymentSession(cart, {
  // ...
  data: {
    setup_future_usage: "off_session",
  },
})
```

You customize the `initiatePaymentSession` function to pass the `data` object with the `setup_future_usage` property. You set the value to `off_session` to allow using the payment method outside of the checkout flow, such as for follow up payments. You can use `on_session` instead if you only want the payment method to be used by the customer during checkout.

By making this change, you always save the payment method that the customer uses during checkout. You can alternatively show a checkbox to confirm saving the payment method, and only pass the `data` object if the customer checks it.

### Test it Out

To test it out, start the Medusa application by running the following command in the Medusa application's directory:

```bash npm2yarn
npm run dev
```

Then, start the Next.js Starter Storefront by running the following command in the storefront's directory:

```bash npm2yarn
npm run dev
```

You can open the storefront in your browser at `localhost:8000`. Then, create a new customer account by clicking on the "Account" link at the top right.

After creating an account and logging in, add a product to the cart and go to the checkout page. Once you get to the payment step, choose Stripe and enter a [test card number](https://docs.stripe.com/testing#cards), such as `4242 4242 4242 4242`.

Then, place the order. Once the order is placed, you can check on the Stripe dashboard that the payment method was saved by:

1. Going to the "Customers" section in the Stripe dashboard.
2. Clicking on the customer you just placed the order with.
3. Scrolling down to the "Payment methods" section. You'll find the payment method you just used to place the order.

![Saved payment method on the Stripe dashboard](https://res.cloudinary.com/dza7lstvk/image/upload/v1745249064/Medusa%20Resources/Screenshot_2025-04-21_at_6.24.00_PM_peotrd.png)

In the next step, you'll show the saved payment methods to the customer during checkout and allow them to select one of them.

***

## Step 5: Use Saved Payment Methods During Checkout

In this step, you'll customize the checkout flow in the Next.js Starter Storefront to show the saved payment methods to the customer and allow them to select one of them to place the order.

### Retrieve Saved Payment Methods

To retrieve the saved payment methods, you'll add a server function that retrieves the customer's saved payment methods from the API route you created earlier.

Add the following in `src/lib/data/payment.ts`:

```ts title="src/lib/data/payment.ts" badgeLabel="Storefront" badgeColor="blue" highlights={paymentHighlights}
export type SavedPaymentMethod = {
  id: string
  provider_id: string
  data: {
    card: {
      brand: string
      last4: string
      exp_month: number
      exp_year: number
    }
  }
}

export const getSavedPaymentMethods = async (accountHolderId: string) => {
  const headers = {
    ...(await getAuthHeaders()),
  }

  return sdk.client.fetch<{
    payment_methods: SavedPaymentMethod[]
  }>(
    `/store/payment-methods/${accountHolderId}`,
    {
      method: "GET",
      headers,
    }
  ).catch(() => {
    return {
      payment_methods: [],
    }
  })
}
```

You define a type for the retrieved payment methods. It contains the following properties:

- `id`: The ID of the payment method in the third-party provider.
- `provider_id`: The ID of the provider in the Medusa application, such as Stripe's ID.
- `data`: Additional data retrieved from the third-party provider related to the saved payment method. The type is modeled after the data returned by Stripe, but you can change it to match other payment providers.

You also create a `getSavedPaymentMethods` function that retrieves the saved payment methods from the API route you created earlier. The function accepts the account holder ID as a parameter and returns the saved payment methods.

### Add Saved Payment Methods Component

Next, you'll add the component that shows the saved payment methods and allows the customer to select one of them.

The component that shows the Stripe card element is defined in `src/modules/checkout/components/payment-container/index.tsx`. So, you'll define the component for the saved payment methods in the same file.

Start by adding the following import statements at the top of the file:

```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { Button } from "@medusajs/ui"
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { SavedPaymentMethod, getSavedPaymentMethods } from "@lib/data/payment"
import { initiatePaymentSession } from "../../../../lib/data/cart"
import { capitalize } from "lodash"
```

Then, update the `PaymentContainerProps` type to include the payment session and cart details:

```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
type PaymentContainerProps = {
  // ...
  paymentSession?: HttpTypes.StorePaymentSession
  cart: HttpTypes.StoreCart
}
```

You'll need these details to find which saved payment method the customer selected, and to initiate a new payment session for the cart when the customer chooses a saved payment method.

Next, add the following component at the end of the file:

```tsx title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={stripeSavedPaymentMethodsHighlights}
const StripeSavedPaymentMethodsContainer = ({
  paymentSession,
  setCardComplete,
  setCardBrand,
  setError,
  cart,
}: {
  paymentSession?: HttpTypes.StorePaymentSession
  setCardComplete: (complete: boolean) => void
  setCardBrand: (brand: string) => void
  setError: (error: string | null) => void
  cart: HttpTypes.StoreCart
}) => {
  const [savedPaymentMethods, setSavedPaymentMethods] = useState<
    SavedPaymentMethod[]
  >([])
  const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<
    string | null
  >(
    paymentSession?.data?.payment_method as string | null
  )

  useEffect(() => {
    const accountHolderId = (
      paymentSession?.context?.account_holder as Record<string, string>
    )
      ?.id

    if (!accountHolderId) {
      return
    }

    getSavedPaymentMethods(accountHolderId)
      .then(({ payment_methods }) => {
        setSavedPaymentMethods(payment_methods)
      })
  }, [paymentSession])

  useEffect(() => {
    if (!selectedPaymentMethod || !savedPaymentMethods.length) {
      setCardComplete(false)
      setCardBrand("")
      setError(null)
      return
    }
    const selectedMethod = savedPaymentMethods.find(
      (method) => method.id === selectedPaymentMethod
    )

    if (!selectedMethod) {
      return
    }

    setCardBrand(capitalize(selectedMethod.data.card.brand))
    setCardComplete(true)
    setError(null)
  }, [selectedPaymentMethod, savedPaymentMethods])

  const handleSelect = async (method: SavedPaymentMethod) => {
    // initiate a new payment session with the selected payment method
    await initiatePaymentSession(cart, {
      provider_id: method.provider_id,
      data: {
        payment_method: method.id,
      },
    }).catch((error) => {
      setError(error.message)
    })

    setSelectedPaymentMethod(method.id)
  }

  if (!savedPaymentMethods.length) {
    return <></>
  }

  // TODO add return statement
}
```

You define a `StripeSavedPaymentMethodsContainer` component that accepts the following props:

- `paymentSession`: The cart's current payment session.
- `setCardComplete`: A function to tell parent components whether the cart or payment method selection is complete. This allows the customer to proceed to the next step in the checkout flow.
- `setCardBrand`: A function to set the brand of the selected payment method. This is useful to show the brand of the selected payment method in review sections of the checkout flow.
- `setError`: A function to set the error message in case of an error.
- `cart`: The cart's details.

In the component, you define a state variable to store the saved payment methods and another one to store the selected payment method.

Then, you use the `useEffect` hook to retrieve the saved payment methods for the account holder set in the cart's payment session. You use the `getSavedPaymentMethods` function you created earlier to retrieve the saved payment methods.

You also use another `useEffect` hook that is executed when the selected payment method changes. In this hook, you check if the selected payment method is valid and set the card brand and completion status accordingly.

Finally, you define a `handleSelect` function that you'll execute when the customer selects a saved payment method. It creates a new payment session with the selected payment method.

To show the saved payment methods, replace the `TODO` with the following `return` statement:

```tsx title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div className="flex flex-col gap-y-2">
    <Text className="txt-medium-plus text-ui-fg-base">
      Choose a saved payment method:
    </Text>
    {savedPaymentMethods.map((method) => (
      <div 
        key={method.id}
        className={`flex items-center justify-between p-4 border rounded-lg cursor-pointer hover:border-ui-border-interactive ${
          selectedPaymentMethod === method.id ? "border-ui-border-interactive" : ""
        }`}
        role="button"
        onClick={() => handleSelect(method)}
      >
        <div className="flex items-center gap-x-4">
          <input
            type="radio"
            name="saved-payment-method" 
            value={method.id}
            checked={selectedPaymentMethod === method.id}
            className="h-4 w-4 text-ui-fg-interactive"
            onChange={(e) => {
              if (e.target.checked) {
                handleSelect(method)
              }
            }}
          />
          <div className="flex flex-col">
            <span className="text-sm font-medium text-ui-fg-base">
              {capitalize(method.data.card.brand)} •••• {method.data.card.last4}
            </span>
            <span className="text-xs text-ui-fg-subtle">
              Expires {method.data.card.exp_month}/{method.data.card.exp_year}
            </span>
          </div>
        </div>
      </div>
    ))}
  </div>
)
```

You display the saved payment methods as radio buttons. When the customer selects one of them, you execute the `handleSelect` function to initiate a new payment session with the selected payment method.

### Modify Existing Stripe Element

Now that you have the component to show the saved payment methods, you need to modify the existing Stripe element to allow customers to select an existing payment method or enter a new one.

In the same `src/modules/checkout/components/payment-container/index.tsx` file, expand the new `paymentSession` and `cart` props of the `StripeCardContainer` component:

```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
export const StripeCardContainer = ({
  // ...
  paymentSession,
  cart,
}: Omit<PaymentContainerProps, "children"> & {
  // ...
}) => {
  // ...
}
```

Then, add a new state variable that keeps track of whether the customer is using a saved payment method or entering a new one:

```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const [isUsingSavedPaymentMethod, setIsUsingSavedPaymentMethod] = useState(
  paymentSession?.data?.payment_method !== null
)
```

Next, add a function that resets the payment session when the customer switches between saved and new payment methods:

```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const handleRefreshSession = async () => {
  await initiatePaymentSession(cart, {
    provider_id: paymentProviderId,
  })
  setIsUsingSavedPaymentMethod(false)
}
```

This function initiates a new payment session for the cart and disables the `isUsingSavedPaymentMethod` state variable.

Finally, replace the `return` statement of the `StripeCardContainer` component with the following:

```tsx title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={stripeCardReturnHighlights}
return (
  <PaymentContainer
    paymentProviderId={paymentProviderId}
    selectedPaymentOptionId={selectedPaymentOptionId}
    paymentInfoMap={paymentInfoMap}
    disabled={disabled}
    paymentSession={paymentSession}
    cart={cart}
  >
    {selectedPaymentOptionId === paymentProviderId &&
      (stripeReady ? (
        <div className="my-4 transition-all duration-150 ease-in-out">
          <StripeSavedPaymentMethodsContainer
            setCardComplete={setCardComplete}
            setCardBrand={setCardBrand}
            setError={setError}
            paymentSession={paymentSession}
            cart={cart}
          />
          {isUsingSavedPaymentMethod && (
            <Button 
              variant="secondary" 
              size="small" 
              className="mt-2" 
              onClick={handleRefreshSession}
            >
              Use a new payment method
            </Button>
          )}
          {!isUsingSavedPaymentMethod && (
            <>
              <Text className="txt-medium-plus text-ui-fg-base my-1">
                Enter your card details:
              </Text>
              <CardElement
                options={useOptions as StripeCardElementOptions}
                onChange={(e) => {
                  setCardBrand(
                    e.brand && e.brand.charAt(0).toUpperCase() + e.brand.slice(1)
                  )
                  setError(e.error?.message || null)
                  setCardComplete(e.complete)
                  }}
                />              
            </>
          )}
        </div>
      ) : (
        <SkeletonCardDetails />
      ))}
  </PaymentContainer>
)
```

You update the `return` statement to:

- Pass the new `paymentSession` and `cart` props to the `PaymentContainer` component.
- Show the `StripeSavedPaymentMethodsContainer` component before Stripe's card element.
- Add a button that's shown when the customer selects a saved payment method. The button allows the customer to switch back to entering a new payment method.

The existing Stripe element in checkout will now show the saved payment methods to the customer along with the component to enter a card's details.

Since you added new props to the `StripeCardContainer` and `PaymentContainer` components, you need to update other components that use them to pass the props.

In `src/modules/checkout/components/payment/index.tsx`, find usages of `StripeCardContainer` and `PaymentContainer` in the return statement and add the `paymentSession` and `cart` props:

```tsx title="src/modules/checkout/components/payment/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"], ["6"], ["11"], ["12"]]}
<div key={paymentMethod.id}>
  {isStripeFunc(paymentMethod.id) ? (
    <StripeCardContainer
      // ...
      paymentSession={activeSession}
      cart={cart}
    />
  ) : (
    <PaymentContainer
      // ...
      paymentSession={activeSession}
      cart={cart}
    />
  )}
</div>
```

### Support Updating Stripe's Client Secret

The Next.js Starter Storefront uses Stripe's `Elements` component to wrap the payment elements. The `Elements` component requires a `clientSecret` prop, which is available in the cart's payment session.

With the recent changes, the client secret will be updated whenever a payment session is initiated, such as when the customer selects a saved payment method. However, the `options.clientSecret` prop of the `Elements` component is immutable, meaning that it cannot be changed after the component is mounted.

To force the component to re-mount and update the `clientSecret` prop, you can add a `key` prop to the `Elements` component. The `key` prop ensures that the `Elements` component re-mounts whenever the client secret changes, allowing Stripe to process the updated payment session.

In `src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx`, find the `Elements` component in the `return` statement and add the `key` prop:

```tsx title="src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"]]}
<Elements 
  options={options} 
  stripe={stripePromise} 
  key={options.clientSecret}
>
  {children}
</Elements>
```

You set the `key` prop to the client secret, which forces the `Elements` component to re-mount whenever the client secret changes.

### Support Payment with Saved Payment Method

The last change you need to make ensures that the customer can place an order with a saved payment method.

When the customer places the order, and they've chosen Stripe as a payment method, the Next.js Starter Storefront uses Stripe's `confirmCardPayment` method to confirm the payment. This method accepts either the ID of a saved payment method, or the details of a new card.

So, you need to update the `confirmCardPayment` usage to support passing the ID of the selected payment method if the customer has selected one.

In `src/modules/checkout/components/payment-button/index.tsx`, find the `handlePayment` method and update its first `if` condition:

```ts title="src/modules/checkout/components/payment-button/index.tsx" badgeLabel="Storefront" badgeColor="blue"
if (!stripe || !elements || (!card && !session?.data.payment_method) || !cart) {
  setSubmitting(false)
  return
}
```

This allows the customer to place their order if they have selected a saved payment method but have not entered a new card.

Then, find the usage of `confirmCardPayment` in the `handlePayment` function and change it to the following:

```ts title="src/modules/checkout/components/payment-button/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={confirmPaymentHighlights}
await stripe
.confirmCardPayment(session?.data.client_secret as string, {
  payment_method: session?.data.payment_method as string || {
    card: card!,
    billing_details: {
      name:
        cart.billing_address?.first_name +
        " " +
        cart.billing_address?.last_name,
      address: {
        city: cart.billing_address?.city ?? undefined,
        country: cart.billing_address?.country_code ?? undefined,
        line1: cart.billing_address?.address_1 ?? undefined,
        line2: cart.billing_address?.address_2 ?? undefined,
        postal_code: cart.billing_address?.postal_code ?? undefined,
        state: cart.billing_address?.province ?? undefined,
      },
      email: cart.email,
      phone: cart.billing_address?.phone ?? undefined,
    },
  },
})
.then(({ error, paymentIntent }) => {
  if (error) {
    const pi = error.payment_intent

    if (
      (pi && pi.status === "requires_capture") ||
      (pi && pi.status === "succeeded")
    ) {
      onPaymentCompleted()
    }

    setErrorMessage(error.message || null)
    return
  }

  if (
    (paymentIntent && paymentIntent.status === "requires_capture") ||
    paymentIntent.status === "succeeded"
  ) {
    return onPaymentCompleted()
  }

  return
})
```

In particular, you're changing the `payment_method` property to either be the ID of the selected payment method, or the details of a new card. This allows the customer to place an order with either a saved payment method or a new one.

### Test it Out

You can now test out placing orders with a saved payment method.

To do that, start the Medusa application by running the following command in the Medusa application's directory:

```bash npm2yarn
npm run dev
```

Then, start the Next.js Starter Storefront by running the following command in the storefront's directory:

```bash npm2yarn
npm run dev
```

In the Next.js Starter Storefront, login with the customer account you created earlier and add a product to the cart.

Then, proceed to the checkout flow. In the payment step, you should see the saved payment method you used earlier. You can select it and place the order.

![Saved payment method in checkout](https://res.cloudinary.com/dza7lstvk/image/upload/v1745308047/Medusa%20Resources/Screenshot_2025-04-21_at_4.07.34_PM_krudjq.png)

Once the order is placed successfully, you can check it in the Medusa Admin dashboard. You can view the order and capture the payment.

![Order in the Medusa Admin dashboard](https://res.cloudinary.com/dza7lstvk/image/upload/v1745318042/Medusa%20Resources/Screenshot_2025-04-22_at_1.33.39_PM_uynlfp.png)

***

## Next Steps

You've added support for saved payment methods in your Medusa application and Next.js Starter Storefront, allowing customers to save their payment methods during checkout and use them in future orders.

You can add more features to the saved payment methods, such as allowing customers to delete saved payment methods. You can use [Stripe's APIs](https://docs.stripe.com/api/payment_methods/detach) in the storefront or add an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) in Medusa to delete the saved payment method.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Integrate Algolia (Search) with Medusa

In this tutorial, you'll learn how to integrate Medusa with Algolia.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. Medusa's architecture supports integrating third-party services, such as a search engine, allowing you to build your unique requirements around core commerce flows.

[Algolia](https://www.algolia.com/doc/) is a search engine that enables you to build and manage an intuitive search experience for your customers. By integrating Algolia with Medusa, you can index e-commerce data, such as products, and allow clients to search through them.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer.

## Summary

By following this tutorial, you'll learn how to:

- Install and set up Medusa.
- Integrate Algolia into Medusa.
- Trigger Algolia reindexing when a product is created, updated, deleted, or when the admin manually triggers a reindex.
- Customize the Next.js Starter Storefront to allow searching for products through Algolia.

![Diagram illustrating the integration of Algolia with Medusa](https://res.cloudinary.com/dza7lstvk/image/upload/v1742889842/Medusa%20Resources/algolia-summary_lhegrr.jpg)

- [Algolia Integration Repository](https://github.com/medusajs/examples/tree/main/algolia-integration): Find the full code for this guide in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1742829748/OpenApi/Algolia-Search_t1zlkd.yaml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Algolia Module

To integrate third-party services into Medusa, you create a custom module. A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

In this step, you'll create a custom module that provides the necessary functionalities to integrate Algolia with Medusa.

Refer to the [Modules documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to learn more.

Before building the module, you need to install Algolia's JavaScript client. Run the following command in your Medusa application's root directory:

```bash npm2yarn
npm install algoliasearch
```

### Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/algolia`.

### Create Service

You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, which is useful if your module defines tables in the database, or connect to a third-party service.

In this section, you'll create the Algolia Module's service and the methods necessary to manage indexed products in Algolia and search through them.

To create the Algolia Module's service, create the file `src/modules/algolia/service.ts` with the following content:

```ts title="src/modules/algolia/service.ts"
import { algoliasearch, SearchClient } from "algoliasearch"

type AlgoliaOptions = {
  apiKey: string;
  appId: string;
  productIndexName: string;
}

export type AlgoliaIndexType = "product"

export default class AlgoliaModuleService {
  private client: SearchClient
  private options: AlgoliaOptions

  constructor({}, options: AlgoliaOptions) {
    this.client = algoliasearch(options.appId, options.apiKey)
    this.options = options
  }

  // TODO add methods
}
```

You export a class that will be the Algolia Module's main service. In the service, you define two properties:

- `client`: An instance of the Algolia Search Client, which you'll use to perform actions with Algolia's API.
- `options`: An object of options that the Module receives when it's registered, which you'll learn about later. The options contain:
  - `apiKey`: The Algolia API key.
  - `appId`: The Algolia App ID.
  - `productIndexName`: The name of the index where products are stored.

If you want to index other types of data, such as product categories, you can add new properties for their index names in the `AlgoliaOptions` type.

A module's service receives the module's options as a second parameter in its constructor. In the constructor, you initialize the Algolia client using the module's options.

A module has a container that holds all resources registered in that module, and you can access those resources in the first parameter of the constructor. Learn more about it in the [Module Container documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md).

#### Index Data Method

The first method you need to add to the service is a method that receives an array of data to add or update in Algolia's index.

Add the following methods to the `AlgoliaModuleService` class:

```ts title="src/modules/algolia/service.ts"
export default class AlgoliaModuleService {
  // ...
  async getIndexName(type: AlgoliaIndexType) {
    switch (type) {
      case "product":
        return this.options.productIndexName
      default:
        throw new Error(`Invalid index type: ${type}`)
    }
  }

  async indexData(data: Record<string, unknown>[], type: AlgoliaIndexType = "product") {
    const indexName = await this.getIndexName(type)
    this.client.saveObjects({
      indexName,
      objects: data.map((item) => ({
        ...item,
        // set the object ID to allow updating later
        objectID: item.id,
      })),
    })
  }
}
```

You define two methods:

1. `getIndexName`: A method that receives an `AlgoliaIndexType` (defined in the previous snippt) and returns the index name for that type. In this case, you only have one type, `product`, so you return the product index name.
   - If you want to index other types of data, you can add more cases to the switch statement.
2. `indexData`: A method that receives an array of data and an `AlgoliaIndexType`. The method indexes the data in the Algolia index for the given type.
   - Notice that you set the `objectID` property of each object to the object's `id`. This ensures that you later update the object instead of creating a new one.

#### Retrieve and Delete Methods

The next methods you'll add to the service are methods to retrieve and delete data from the Algolia index. You'll see their use later as you keep the Algolia index in sync with Medusa.

Add the following methods to the `AlgoliaModuleService` class:

```ts title="src/modules/algolia/service.ts"
export default class AlgoliaModuleService {
  // ...

  async retrieveFromIndex(objectIDs: string[], type: AlgoliaIndexType = "product") {
    const indexName = await this.getIndexName(type)
    return await this.client.getObjects<Record<string, unknown>>({
      requests: objectIDs.map((objectID) => ({
        indexName,
        objectID,
      })),
    })
  }

  async deleteFromIndex(objectIDs: string[], type: AlgoliaIndexType = "product") {
    const indexName = await this.getIndexName(type)
    await this.client.deleteObjects({
      indexName,
      objectIDs,
    })
  }
}
```

You define two methods:

1. `retrieveFromIndex`: A method that receives an array of object IDs and an `AlgoliaIndexType`. The method retrieves the objects with the given IDs from the Algolia index.
2. `deleteFromIndex`: A method that receives an array of object IDs and an `AlgoliaIndexType`. The method deletes the objects with the given IDs from the Algolia index.

#### Search Method

The last method you'll implement is a method to search through the Algolia index. You'll later use this method to expose the search functionality to clients, such as the Next.js Starter Storefront.

Add the following method to the `AlgoliaModuleService` class:

```ts title="src/modules/algolia/service.ts"
export default class AlgoliaModuleService {
  // ...

  async search(query: string, type: AlgoliaIndexType = "product") {
    const indexName = await this.getIndexName(type)
    return await this.client.search({
      requests: [
        {
          indexName,
          query,
        },
      ],
    })
  }
}
```

The `search` method receives a query string and an `AlgoliaIndexType`. The method searches through the Algolia index for the given type, such as products, and returns the results.

### Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/algolia/index.ts` with the following content:

```ts title="src/modules/algolia/index.ts"
import { Module } from "@medusajs/framework/utils"
import AlgoliaModuleService from "./service"

export const ALGOLIA_MODULE = "algolia"

export default Module(ALGOLIA_MODULE, {
  service: AlgoliaModuleService,
})
```

You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `algolia`.
2. An object with a required property `service` indicating the module's service.

You also export the module's name as `ALGOLIA_MODULE` so you can reference it later.

### Add Module to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/algolia",
      options: {
        appId: process.env.ALGOLIA_APP_ID!,
        apiKey: process.env.ALGOLIA_API_KEY!,
        productIndexName: process.env.ALGOLIA_PRODUCT_INDEX_NAME!,
      },
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

You also pass an `options` property with the module's options, including the Algolia App ID, API Key, and the product index name.

### Add Environment Variables

Before you can start using the Algolia Module, you need to set the environment variables for the Algolia App ID, API Key, and the product index name.

Add the following environment variables to your `.env` file:

```env
ALGOLIA_APP_ID=your-algolia-app-id
ALGOLIA_API_KEY=your-algolia-api-key
ALGOLIA_PRODUCT_INDEX_NAME=your-product-index-name
```

Where:

- `your-algolia-app-id` is your Algolia App ID. You can retrieve it from the Algolia dashboard by clicking at the application ID at the top left next to the sidebar. The pop up will show the application ID below the application's name.

![Find the Algolia App ID by clicking on the application name in the Algolia dashboard, then copying the ID below the name in the pop-up](https://res.cloudinary.com/dza7lstvk/image/upload/v1742815360/Medusa%20Resources/Screenshot_2025-03-24_at_1.19.30_PM_kdp3y5.png)

- `your-algolia-api-key` is your Algolia API Key. To retrieve it from the Algolia dashboard:
  1. Click on Settings in the sidebar.
  2. Choose API Keys under "Team and Access".

![In the settings page, find the Team and Access section at the right of the page and choose API Keys](https://res.cloudinary.com/dza7lstvk/image/upload/v1742815534/Medusa%20Resources/Screenshot_2025-03-24_at_1.25.09_PM_hwsiba.png)

3. Copy the Admin API Key.

- `your-product-index-name` is the name of the index where you'll store products. You can find it by going to Search -> Index, and copying the index name at the top of the page.

![In the Algolia dashboard, go to Search -> Index and copy the index name at the top of the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1742815790/Medusa%20Resources/Screenshot_2025-03-24_at_1.28.58_PM_yq10sf.png)

Your module is now ready for use. You'll see how to use it in the next steps.

***

## Step 3: Sync Products to Algolia Workflow

To keep the Algolia index in sync with Medusa, you need to trigger indexing when products are created, updated, or deleted in Medusa. You can also allow the admin to manually trigger a reindex.

To implement the indexing functionality, you need to create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features.

Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

In this step, you'll create a workflow that indexes products in Algolia. In the next steps, you'll learn how to use the workflow when products are created, updated, or deleted, or when the admin manually triggers a reindex.

The workflow has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve products matching specified filters and pagination parameters.
- [syncProductsStep](#syncProductsStep): Index published products in Algolia.
- [deleteProductsFromAlgoliaStep](#deleteProductsFromAlgoliaStep): Delete unpublished products from Algolia.

Medusa provides the `useQueryGraphStep` in its `@medusajs/medusa/core-flows` package. So, you only need to implement the other steps.

### syncProductsStep

In the second step of the workflow, you create or update indexes in Algolia for the products retrieved in the first step.

To create the step, create the file `src/workflows/steps/sync-products.ts` with the following content:

If you get a type error on resolving the Algolia Module, run the Medusa application once with the `npm run dev` or `yarn dev` command to generate the necessary type definitions, as explained in the [Automatically Generated Types guide](https://docs.medusajs.com/docs/learn/fundamentals/generated-types/index.html.md).

```ts title="src/workflows/steps/sync-products.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { ALGOLIA_MODULE } from "../../modules/algolia"
import AlgoliaModuleService from "../../modules/algolia/service"

export type SyncProductsStepInput = {
  products: {
    id: string
    title: string
    description?: string
    handle: string
    thumbnail?: string
    categories: {
      id: string
      name: string
      handle: string
    }[]
    tags: {
      id: string
      value: string
    }[]
  }[]
}

export const syncProductsStep = createStep(
  "sync-products",
  async ({ products }: SyncProductsStepInput, { container }) => {
    const algoliaModuleService: AlgoliaModuleService = container.resolve(ALGOLIA_MODULE)

    const existingProducts = (await algoliaModuleService.retrieveFromIndex(
      products.map((product) => product.id),
      "product"
    )).results.filter(Boolean)
    const newProducts = products.filter(
      (product) => !existingProducts.some((p) => p.objectID === product.id)
    )
    await algoliaModuleService.indexData(
      products as unknown as Record<string, unknown>[], 
      "product"
    )

    return new StepResponse(undefined, {
      newProducts: newProducts.map((product) => product.id),
      existingProducts,
    })
  }
  // TODO add compensation
)
```

You create a step with `createStep` from the Workflows SDK. It accepts two parameters:

1. The step's unique name, which is `sync-products`.
2. An async function that receives two parameters:
   - The step's input, which is in this case an object holding an array of products to sync into Algolia.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.

In the step function, you resolve the Algolia Module's service from the Medusa container using the name you exported in the module definition's file.

Then, you retrieve the products that are already indexed in Algolia and determine which products are new. You'll learn why this is useful in a bit.

Finally, you pass the products you received in the input to Algolia to create or update its indices.

A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters:

1. The step's output, which in this case is `undefined`.
2. Data to pass to the step's compensation function.

#### Compensation Function

The compensation function undoes the actions performed in a step. Then, if an error occurs during the workflow's execution, the compensation functions of executed steps are called to roll back the changes. This mechanism ensures data consistency in your application, especially as you integrate external systems.

To add a compensation function to a step, pass it as a third parameter to `createStep`:

```ts title="src/workflows/steps/sync-products.ts"
export const syncProductsStep = createStep(
  // ...
  async (input, { container }) => {
    if (!input) {
      return
    }

    const algoliaModuleService: AlgoliaModuleService = container.resolve(ALGOLIA_MODULE)
    
    if (input.newProducts) {
      await algoliaModuleService.deleteFromIndex(
        input.newProducts,
        "product"
      )
    }

    if (input.existingProducts) {
      await algoliaModuleService.indexData(
        input.existingProducts,
        "product"
      )
    }
  }
)
```

The compensation function receives two parameters:

1. The data you passed as a second parameter of `StepResponse` in the step function.
2. A context object similar to the step function that holds the Medusa container.

In the compensation function, you resolve the Algolia Module's service from the container. Then, you delete from Algolia the products that were newly indexed, and revert the existing products to their original data.

#### deleteProductsFromAlgoliaStep

The `deleteProductsFromAlgoliaStep` step deletes products from Algolia by their IDs.

Create the step at `src/workflows/steps/delete-products-from-algolia.ts` with the following content:

```ts title="src/workflows/steps/delete-products-from-algolia.ts"
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { ALGOLIA_MODULE } from "../../modules/algolia"

export type DeleteProductsFromAlgoliaWorkflow = {
  ids: string[]
}

export const deleteProductsFromAlgoliaStep = createStep(
  "delete-products-from-algolia-step",
  async (
    { ids }: DeleteProductsFromAlgoliaWorkflow,
    { container }
  ) => {
    const algoliaModuleService = container.resolve(ALGOLIA_MODULE)
    
    const existingRecords = await algoliaModuleService.retrieveFromIndex(
      ids, 
      "product"
    )
    await algoliaModuleService.deleteFromIndex(
      ids,
      "product"
    )

    return new StepResponse(undefined, existingRecords)
  },
  async (existingRecords, { container }) => {
    if (!existingRecords) {
      return
    }
    const algoliaModuleService = container.resolve(ALGOLIA_MODULE)
    
    await algoliaModuleService.indexData(
      existingRecords as unknown as Record<string, unknown>[],
      "product"
    )
  }
)
```

The step receives the IDs of the products to delete as an input.

In the step, you resolve the Algolia Module's service and retrieve the existing records from Algolia. This is useful to revert the deletion if an error occurs.

Then, you delete the products from Algolia and pass the existing records to the compensation function.

In the compensation function, you reindex the existing records if an error occurs.

### Add Sync Products Workflow

You can now create the workflow that syncs the products to Algolia.

To create the workflow, create the file `src/workflows/sync-products.ts` with the following content:

```ts title="src/workflows/sync-products.ts"
import { 
  createWorkflow, 
  transform, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { syncProductsStep, SyncProductsStepInput } from "./steps/sync-products"
import { deleteProductsFromAlgoliaStep } from "./steps/delete-products-from-algolia"

type SyncProductsWorkflowInput = {
  filters?: Record<string, unknown>
  limit?: number
  offset?: number
}

export const syncProductsWorkflow = createWorkflow(
  "sync-products",
  ({ filters, limit, offset }: SyncProductsWorkflowInput) => {
    const { data: products, metadata } = useQueryGraphStep({
      entity: "product",
      fields: [
        "id", 
        "title", 
        "description", 
        "handle", 
        "thumbnail", 
        "categories.id",
        "categories.name",
        "categories.handle",
        "tags.id",
        "tags.value",
        "status",
      ],
      pagination: {
        take: limit,
        skip: offset,
      },
      filters,
    })

    const {
      publishedProducts,
      unpublishedProductsToDelete,
    } = transform({
      products,
    }, (data) => {
      const publishedProducts: SyncProductsStepInput["products"] = []
      const unpublishedProductsToDelete: string[] = []

      data.products.forEach((product) => {
        if (product.status === "published") {
          const { status, ...rest } = product
          publishedProducts.push(rest as SyncProductsStepInput["products"][0])
        } else {
          unpublishedProductsToDelete.push(product.id)
        }
      })

      return {
        publishedProducts,
        unpublishedProductsToDelete,
      }
    })

    syncProductsStep({
      products: publishedProducts,
    })

    deleteProductsFromAlgoliaStep({
      ids: unpublishedProductsToDelete,
    })

    return new WorkflowResponse({
      products,
      metadata,
    })
  }
)
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is pagination and filter parameters for the products to retrieve.

In the workflow's constructor function, you:

1. Execute `useQueryGraphStep` to retrieve products from Medusa's database. This step uses Medusa's [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) tool to retrieve data across modules. You pass it the pagination and filter parameters you received in the input.
2. Use [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) to prepare two lists: one for published products to index in Algolia and another for unpublished products to delete from Algolia.
3. Execute `syncProductsStep` to index the published products in Algolia.
4. Execute `deleteProductsFromAlgoliaStep` to delete unpublished products from Algolia.

A workflow must return an instance of `WorkflowResponse`. The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is an object holding the retrieved products and their pagination details.

In the next step, you'll learn how to execute this workflow.

***

## Step 4: Trigger Algolia Sync Manually

As mentioned earlier, you'll trigger the Algolia sync automatically when product events occur, but you also want to allow the admin to manually trigger a reindex.

In this step, you'll add the functionality to trigger the `syncProductsWorkflow` manually from the Medusa Admin dashboard. This requires:

1. Creating a subscriber that listens to a custom `algolia.sync` event to trigger syncing products to Algolia.
2. Creating an API route that the Medusa Admin dashboard can call to emit the `algolia.sync` event, which triggers the subscriber.
3. Add a new page or UI route to the Medusa Admin dashboard to allow the admin to trigger the reindex.

### Create Products Sync Subscriber

A subscriber is an asynchronous function that listens to one or more events and performs actions when these events are emitted. A subscriber is useful when syncing data across systems, as the operation can be time-consuming and should be performed in the background.

Learn more about subscribers in the [Events and Subscribers documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).

You create a subscriber in a TypeScript or JavaScript file under the `src/subscribers` directory. So, to create the subscriber that listens to the `algolia.sync` event, create the file `src/subscribers/algolia-sync.ts` with the following content:

```ts title="src/subscribers/algolia-sync.ts"
import {
  SubscriberArgs,
  type SubscriberConfig,
} from "@medusajs/framework"
import { syncProductsWorkflow } from "../workflows/sync-products"

export default async function algoliaSyncHandler({ 
  container,
}: SubscriberArgs) {
  const logger = container.resolve("logger")
  
  let hasMore = true
  let offset = 0
  const limit = 50
  let totalIndexed = 0

  logger.info("Starting product indexing...")

  while (hasMore) {
    const { result: { products, metadata } } = await syncProductsWorkflow(container)
      .run({
        input: {
          limit,
          offset,
        },
      })

    hasMore = offset + limit < (metadata?.count ?? 0)
    offset += limit
    totalIndexed += products.length
  }

  logger.info(`Successfully indexed ${totalIndexed} products`)
}

export const config: SubscriberConfig = {
  event: "algolia.sync",
}
```

A subscriber file must export:

1. An asynchronous function, which is the subscriber that is executed when the event is emitted.
2. A configuration object that holds the name of the event the subscriber listens to, which is `algolia.sync` in this case.

The subscriber function receives an object as a parameter that has a `container` property, which is the Medusa container.

In the subscriber function, you initialize variables to keep track of the pagination and the total number of products indexed.

Then, you start a loop that retrieves products in batches of 50 and indexes them in Algolia using the `syncProductsWorkflow`. Finally, you log the total number of products indexed.

You'll learn how to emit the `algolia.sync` event next.

If you want to sync other data types, you can do it in this subscriber as well.

### Create API Route to Trigger Sync

To allow the Medusa Admin dashboard to trigger the `algolia.sync` event, you need to create an API route that emits the event.

An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts.

Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

So, to create an API route at the path `/admin/algolia/sync`, create the file `src/api/admin/algolia/sync/route.ts` with the following content:

```ts title="src/api/admin/algolia/sync/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"

export async function POST(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const eventModuleService = req.scope.resolve(Modules.EVENT_BUS)
  await eventModuleService.emit({
    name: "algolia.sync",
    data: {},
  })
  res.send({
    message: "Syncing data to Algolia",
  })
}
```

Since you export a `POST` route handler function, you expose a `POST` API route at `/admin/algolia/sync`. The route handler function accepts two parameters:

1. A request object with details and context on the request, such as body parameters or authenticated user details.
2. A response object to manipulate and send the response.

In the route handler, you use the Medusa container that is available in the request object to resolve the [Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/index.html.md). This module manages events and their subscribers.

Then, you emit the `algolia.sync` event using the Event Module's `emit` method, passing it the event name.

Finally, you send a response with a message indicating that data is being synced to Algolia.

### Add Algolia Sync Page to Admin Dashboard

The last step is to add a new page to the admin dashboard that allows the admin to trigger the reindex. You add a new page using a [UI Route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md).

A UI route is a React component that specifies the content to be shown in a new page in the Medusa Admin dashboard. You'll create a UI route to display a button that triggers the reindex when clicked.

Learn more about UI routes in the [UI Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md).

#### Configure JS SDK

Before creating the UI route, you'll configure Medusa's [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) that you can use to send requests to the Medusa server from any client application, including your Medusa Admin customizations.

The JS SDK is installed by default in your Medusa application. To configure it, create the file `src/admin/lib/sdk.ts` with the following content:

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: "http://localhost:9000",
  debug: process.env.NODE_ENV === "development",
  auth: {
    type: "session",
  },
})
```

You create an instance of the JS SDK using the `Medusa` class from the JS SDK. You pass it an object having the following properties:

- `baseUrl`: The base URL of the Medusa server.
- `debug`: A boolean indicating whether to log debug information into the console.
- `auth`: An object specifying the authentication type. When using the JS SDK for admin customizations, you use the `session` authentication type.

#### Create UI Route

You'll now create the UI route that displays a button to trigger the reindex. You create a UI route in a `page.tsx` file under a sub-directory of `src/admin/routes` directory. The file's path relative to `src/admin/routes` determines its path in the dashboard.

So, to create a new page under the Settings section of the Medusa Admin, create the file `src/admin/routes/settings/algolia/page.tsx` with the following content:

```tsx title="src/admin/routes/settings/algolia/page.tsx"
import { Container, Heading, Button, toast } from "@medusajs/ui"
import { useMutation } from "@tanstack/react-query"
import { sdk } from "../../../lib/sdk"
import { defineRouteConfig } from "@medusajs/admin-sdk"

const AlgoliaPage = () => {
  const { mutate, isPending } = useMutation({
    mutationFn: () => 
      sdk.client.fetch("/admin/algolia/sync", {
        method: "POST",
      }),
    onSuccess: () => {
      toast.success("Successfully triggered data sync to Algolia") 
    },
    onError: (err) => {
      console.error(err)
      toast.error("Failed to sync data to Algolia") 
    },
  })

  const handleSync = () => {
    mutate()
  }

  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">Algolia Sync</Heading>
      </div>
      <div className="px-6 py-8">
        <Button 
          variant="primary"
          onClick={handleSync}
          isLoading={isPending}
        >
          Sync Data to Algolia
        </Button>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Algolia",
})

export default AlgoliaPage
```

A UI route's file must export:

1. A React component that defines the content of the page.
2. A configuration object that specifies the route's label in the dashboard. This label is used to show a sidebar item for the new route.

In the React component, you use `useMutation` hook from `@tanstack/react-query` to create a mutation that sends a `POST` request to the API route you created earlier. In the mutation function, you use the JS SDK to send the request.

Then, in the return statement, you display a button that triggers the mutation when clicked, which sends a request to the API route you created earlier.

### Test it Out

You'll now test out the entire flow, starting from triggering the reindex manually from the Medusa Admin dashboard, to checking the Algolia dashboard for the indexed products.

Run the following command to start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin at `http://localhost:9000/app` and log in with the credentials you set up in the first step.

Can't remember the credentials? Learn how to create a user in the [Medusa CLI reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-cli/commands/user/index.html.md).

After you log in, go to Settings from the sidebar. You'll find in the Settings' sidebar a new "Algolia" item. If you click on it, you'll find the page you created with the button to sync products to Algolia.

If you click on the button, the products will be synced to Algolia.

![The Algolia Sync page in the Medusa Admin dashboard with a button to sync products to Algolia](https://res.cloudinary.com/dza7lstvk/image/upload/v1742820813/Medusa%20Resources/Screenshot_2025-03-24_at_2.52.31_PM_eiegzb.png)

You can check that the sync ran and was completed by checking the Medusa logs in the terminal where you started the Medusa application. You should find the following messages:

```bash
info:    Processing algolia.sync which has 1 subscribers
info:    Starting product indexing...
info:    Successfully indexed 4 products
```

These messages indicate that the `algolia.sync` event was emitted, which triggered the subscriber you created to sync the products using the `syncProductsWorkflow`.

Finally, you can check the Algolia dashboard to see the indexed products. Go to Search -> Index, and check the records of the index you set up in the Algolia Module's options (`products`, for example).

![The Algolia dashboard showing the indexed products](https://res.cloudinary.com/dza7lstvk/image/upload/v1742821034/Medusa%20Resources/Screenshot_2025-03-24_at_2.56.38_PM_mtojrv.png)

***

## Step 5: Update Index on Product Changes

You'll now automate the indexing of the products whenever a change occurs. That includes when a product is created, updated, or deleted.

Similar to before, you'll create subscribers to listen to these events.

### Handle Create and Update Products

The action to perform when a product is created or updated is the same. You'll use the `syncProductsWorkflow` to sync the product to Algolia.

So, you only need one subscriber to handle these two events. To create the subscriber, create the file `src/subscribers/product-sync.ts` with the following content:

```ts title="src/subscribers/product-sync.ts"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { syncProductsWorkflow } from "../workflows/sync-products"

export default async function handleProductEvents({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await syncProductsWorkflow(container)
    .run({
      input: {
        filters: {
          id: data.id,
        },
      },
    })
}

export const config: SubscriberConfig = {
  event: ["product.created", "product.updated"],
}
```

The subscriber listens to the `product.created` and `product.updated` events. When either of these events is emitted, the subscriber triggers the `syncProductsWorkflow` to sync the product to Algolia.

When the `product.created` and `product.updated` events are emitted, the product's ID is passed in the event data payload, which you can access in the `event.data` property of the subscriber function's parameter.

So, you pass the product's ID to the `syncProductsWorkflow` as a filter to retrieve only the product that was created or updated.

#### Test it Out

To test it out, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, either create a product or update an existing one using the Medusa Admin dashboard. If you check the Algolia dashboard, you'll find that the product was created or updated.

### Handle Product Deletion

When a product is deleted, you need to remove it from the Algolia index. As this requires a different action than creating or updating a product, you'll create a new workflow that deletes the product from Algolia, then create a subscriber that listens to the `product.deleted` event to trigger the workflow.

#### Create Delete Product Workflow

You've already created the `deleteProductsFromAlgoliaStep` that deletes products from Algolia by their IDs. So, you can create the workflow that uses this step.

Create the file `src/workflows/delete-products-from-algolia.ts` with the following content:

```ts title="src/workflows/delete-products-from-algolia.ts"
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
import { deleteProductsFromAlgoliaStep } from "./steps/delete-products-from-algolia"

type DeleteProductsFromAlgoliaWorkflowInput = {
  ids: string[]
}

export const deleteProductsFromAlgoliaWorkflow = createWorkflow(
  "delete-products-from-algolia",
  (input: DeleteProductsFromAlgoliaWorkflowInput) => {
    deleteProductsFromAlgoliaStep(input)
  }
)
```

The workflow receives an object with the IDs of the products to delete. It then executes the `deleteProductsFromAlgoliaStep` to delete the products from Algolia.

#### Create Delete Product Subscriber

Finally, you'll create the subscriber that listens to the `product.deleted` event to trigger the above workflow.

Create the file `src/subscribers/product-delete.ts` with the following content:

```ts title="src/subscribers/product-delete.ts"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { deleteProductsFromAlgoliaWorkflow } from "../workflows/delete-products-from-algolia"

export default async function handleProductDeleted({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await deleteProductsFromAlgoliaWorkflow(container)
    .run({
      input: {
        ids: [data.id],
      },
    })
}

export const config: SubscriberConfig = {
  event: "product.deleted",
}
```

The subscriber listens to the `product.deleted` event. When the event is emitted, the subscriber triggers the `deleteProductsFromAlgoliaWorkflow`, passing it the ID of the product to delete.

#### Test it Out

To test product deletion, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, delete a product from the Medusa Admin dashboard. If you check the Algolia dashboard, you'll find that the product was deleted there as well.

***

## Step 6: Add Search API Route

Before customizing the storefront to show the search UI, you'll create an API route in your Medusa application that allows storefronts to search products in Algolia.

While you can implement the search functionality directly in the storefront to interact with Algolia, this approach centralizes your search integration in Medusa, allowing you to change or modify the integration as necessary. You can also rely on the same behavior and results across different storefronts.

To implement the API Route, create the file `src/api/store/products/search/route.ts` with the following content:

```ts title="src/api/store/products/search/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { ALGOLIA_MODULE } from "../../../../modules/algolia"
import AlgoliaModuleService from "../../../../modules/algolia/service"
import { z } from "zod"

export const SearchSchema = z.object({
  query: z.string(),
})

type SearchRequest = z.infer<typeof SearchSchema>

export async function POST(
  req: MedusaRequest<SearchRequest>,
  res: MedusaResponse
) {
  const algoliaModuleService: AlgoliaModuleService = req.scope.resolve(ALGOLIA_MODULE)

  const { query } = req.validatedBody

  const results = await algoliaModuleService.search(
    query as string 
  )

  res.json(results)
}
```

You first define a schema with [Zod](https://zod.dev/), a library to define validation schemas. The schema defines the structure of the request body, which in this case is an object with a `query` property of type `string`. Later, you'll use the schema to enforce request body validation.

Then, you expose a `POST` API Route at `/store/search` that searches Algolia using the `search` method you implemented in the Algolia Module's service. You pass to it the query string from the request body.

Finally, you return the search results as it is in the response.

### Add Validation Middleware

To ensure that requests sent to the API route have the required request body parameters, you can use a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md). A middleware is a function executed when a request is sent to an API Route. It's executed before the route handler.

Learn more about middleware in the [Middlewares documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

Middlewares are created in the `src/api/middlewares.ts` file. So create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  validateAndTransformBody, 
} from "@medusajs/framework/http"
import { SearchSchema } from "./store/products/search/route"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/store/products/search",
      method: ["POST"],
      middlewares: [
        validateAndTransformBody(SearchSchema),
      ],
    },
  ],
})
```

To export the middlewares, you use the `defineMiddlewares` function. It accepts an object having a `routes` property, whose value is an array of middleware route objects. Each middleware route object has the following properties:

- `matcher`: The path of the route the middleware applies to.
- `method`: The HTTP methods the middleware applies to, which is in this case `POST`.
- `middlewares`: An array of middleware functions to apply to the route. You apply the `validateAndTransformBody` middleware which ensures that a request's body has the required parameters. You pass it the schema you defined earlier in the API route's file.

You can use the search API route now. You'll see it in action as you customize the storefront in the next step.

***

## Step 7: Search Products in Next.js Starter Storefront

The last step is to provide the search functionalities to customers on your storefront. In the first step, you installed the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) along with the Medusa application.

In this step, you'll customize the Next.js Starter Storefront to add the search functionality.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-search`, you can find the storefront by going back to the parent directory and changing to the `medusa-search-storefront` directory:

```bash
cd ../medusa-search-storefront # change based on your project name
```

### Install Algolia Packages

Before adding the implementation of the search functionality, you need to install the Algolia packages necessary to add the search functionality in your storefront.

Run the following command in the directory of your Next.js Starter Storefront:

```bash npm2yarn
npm install algoliasearch react-instantsearch
```

This installs the Algolia Search JavaScript client and the React InstantSearch library, which you'll use to build the search functionality.

### Add Search Client Configuration

Next, you need to configure the search client. Not only do you need to initialize Algolia, but you also need to change the searching mechanism to use your custom API route in the Medusa application instead of Algolia's API directly.

In `src/lib/config.ts`, add the following imports at the top of the file:

```ts title="src/lib/config.ts" badgeLabel="Storefront" badgeColor="blue"
import { 
  liteClient as algoliasearch, 
  LiteClient as SearchClient,
} from "algoliasearch/lite"
```

Then, add the following at the end of the file:

```ts title="src/lib/config.ts" badgeLabel="Storefront" badgeColor="blue"
export const searchClient: SearchClient = {
  ...(algoliasearch(
    process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || "", 
    process.env.NEXT_PUBLIC_ALGOLIA_API_KEY || ""
  )),
  search: async (params) => {
    const request = Array.isArray(params) ? params[0] : params
    const query = "params" in request ? request.params?.query : 
      "query" in request ? request.query : ""

    if (!query) {
      return {
        results: [
          {
            hits: [],
            nbHits: 0,
            nbPages: 0,
            page: 0,
            hitsPerPage: 0,
            processingTimeMS: 0,
            query: "",
            params: "",
          },
        ],
      }
    }

    return await sdk.client.fetch(`/store/products/search`, {
      method: "POST",
      body: {
        query,
      },
    })
  },
}
```

In the code above, you create a `searchClient` object that initializes the Algolia client with your Algolia App ID and API Key.

You also define a `search` method that sends a `POST` request to the search API route you created in the Medusa application. You use the JS SDK (which is initialized in this same file) to send the request.

### Set Environment Variables

In the storefront's `.env.local` file, add the following Algolia-related environment variables:

```plain badgeLabel="Storefront" badgeColor="blue"
NEXT_PUBLIC_ALGOLIA_APP_ID=your_algolia_app_id
NEXT_PUBLIC_ALGOLIA_API_KEY=your_algolia_api_key
NEXT_PUBLIC_ALGOLIA_PRODUCT_INDEX_NAME=your-products-index-name
```

Where:

- `your_algolia_app_id` is your Algolia App ID, as explained in the [Add Environment Variables section](#add-environment-variables) earlier.
- `your_algolia_api_key` is your Algolia Search API key. You can retrieve it from the [same API keys page on the Algolia dashboard](#add-environment-variables) that you retrieved the Admin API key from.
- `your-products-index-name` is the name of the index you created in Algolia to store the products, which you can retrieve as explained in the [Add Environment Variables section](#add-environment-variables) earlier. You'll use this variable later.

Do not expose your Admin API key in the storefront. The Admin API key should only be used in the Medusa application to interact with Algolia, as it has full access to your Algolia account.

### Add Search Modal Component

You'll now add a search modal component that customers can use to search for products. The search modal will display the search results in real-time as the customer types in the search query.

Later, you'll add the search modal to the navigation bar, allowing customers to open the search modal from any page.

Create the file `src/modules/search/components/modal/index.tsx` with the following content:

```tsx title="src/modules/search/components/modal/index.tsx" badgeLabel="Storefront" badgeColor="blue"
"use client"

import React, { useEffect, useState } from "react"
import { Hits, InstantSearch, SearchBox } from "react-instantsearch"
import { searchClient } from "../../../../lib/config"
import Modal from "../../../common/components/modal"
import { Button } from "@medusajs/ui"
import Image from "next/image"
import Link from "next/link"
import { usePathname } from "next/navigation"

type Hit = {
  objectID: string;
  id: string;
  title: string;
  description: string;
  handle: string;
  thumbnail: string;
}

export default function SearchModal() {
  const [isOpen, setIsOpen] = useState(false)
  const pathname = usePathname()

  useEffect(() => {
    setIsOpen(false)
  }, [pathname])

  return (
    <>
      <div className="hidden small:flex items-center gap-x-6 h-full">
        <Button 
          onClick={() => setIsOpen(true)} 
          variant="transparent"
          className="hover:text-ui-fg-base text-small-regular px-0 hover:bg-transparent focus:!bg-transparent"
        >
          Search
        </Button>
      </div>
      <Modal isOpen={isOpen} close={() => setIsOpen(false)}>
        <InstantSearch 
          searchClient={searchClient} 
          indexName={process.env.NEXT_PUBLIC_ALGOLIA_PRODUCT_INDEX_NAME}
        >
          <SearchBox className="w-full [&_input]:w-[94%] [&_input]:outline-none [&_button]:w-[3%]" />
          <Hits hitComponent={Hit} />
        </InstantSearch>
      </Modal>
    </>
  )
}

const Hit = ({ hit }: { hit: Hit }) => {
  return (
    <div className="flex flex-row gap-x-2 mt-4 relative">
      <Image src={hit.thumbnail} alt={hit.title} width={100} height={100} />
      <div className="flex flex-col gap-y-1">
        <h3>{hit.title}</h3>
        <p className="text-sm text-gray-500">{hit.description}</p>
      </div>
      <Link href={`/products/${hit.handle}`} className="absolute right-0 top-0 w-full h-full" aria-label={`View Product: ${hit.title}`} />
    </div>
  )
}
```

You create a `SearchModal` component that displays a search box and the search results using widgets from Algolia's `react-instantsearch` library.

To display each result item (or hit), you create a `Hit` component that displays the product's title, description, and thumbnail. You also add a link to the product's page.

Finally, you show the search modal when the customer clicks a "Search" button, which you'll add to the navigation bar next.

### Add Search Button to Navigation Bar

The last step is to show the search button in the navigation bar.

In `src/modules/layout/templates/nav/index.tsx`, add the following imports at the top of the file:

```tsx title="src/modules/layout/templates/nav/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import SearchModal from "@modules/search/components/modal"
```

Then, in the return statement of the `Nav` component, add the `SearchModal` component before the `div` surrounding the "Account" link:

```tsx title="src/modules/layout/templates/nav/index.tsx" badgeLabel="Storefront" badgeColor="blue"
<SearchModal />
```

The search button will now appear in the navigation bar before the Account link.

### Test it Out

To test out the storefront changes and the search API route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, start the Next.js Starter Storefront from its directory:

```bash npm2yarn
npm run dev
```

Next, go to `localhost:8000`. You'll find a Search button at the top right of the navigation bar. If you click on it, you can search through your products. You can also click on a product to view its page.

![The Next.js Starter Storefront showing the search modal with search results](https://res.cloudinary.com/dza7lstvk/image/upload/v1742827777/Medusa%20Resources/Screenshot_2025-03-24_at_4.49.23_PM_kzhldx.png)

***

## Next Steps

You've now integrated Algolia with Medusa and added search functionality to your storefront. You can expand on these features to:

- Add filters to the search results. You can do that using Algolia's [widgets](https://www.algolia.com/doc/guides/building-search-ui/widgets/showcase/react/) and customizing the search API route in Medusa to accept filter parameters.
- Support indexing other data types, such as product categories. You can create the subscribers and workflows for categories similar to products.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Integrate Avalara (AvaTax) for Tax Calculation

In this tutorial, you'll learn how to integrate Avalara with Medusa to handle tax calculations.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. Medusa's architecture supports integrating third-party services, such as tax providers, allowing you to build custom features around core commerce flows.

[Avalara](https://www.avalara.com/) is a leading provider of tax compliance solutions, including sales tax calculation, filing, and remittance. By integrating Avalara with Medusa, you can calculate taxes during checkout with accurate rates based on customer location.

## Summary

By following this tutorial, you'll learn how to:

- Install and set up Medusa.
- Create the Avalara Tax Module Provider that calculates taxes using Avalara.
- Create transactions in Avalara when an order is placed.
- Sync products to Avalara to manage their tax codes and classifications.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Diagram showing Avalara integration with Medusa for tax calculation during checkout](https://res.cloudinary.com/dza7lstvk/image/upload/v1760969087/Medusa%20Resources/avalara-summary_rqrkpf.jpg)

[Example Repository](https://github.com/medusajs/examples/tree/main/avalara-integration): Find the full code of the guide in this repository.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`.

The Medusa application consists of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Avalara Tax Module Provider

To integrate third-party services into Medusa, you create a custom module. A module is a reusable package with functionalities related to a single feature or domain.

Medusa's [Tax Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/index.html.md) implements concepts and functionalities related to taxes, but delegates tax calculations to external services through Tax Module Providers.

In this step, you'll integrate Avalara as a Tax Module Provider. Later, you'll use it to calculate taxes in your Medusa application.

Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules in Medusa.

### a. Install AvaTax SDK

First, install the AvaTax SDK package to interact with Avalara's API. Run the following command in your Medusa application's directory:

```bash npm2yarn
npm install avatax
```

### b. Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/avalara`.

### c. Define Module Options

Next, define a TypeScript type for the Avalara Module options. These options configure the module when it's registered in the Medusa application.

Create the file `src/modules/avalara/types.ts` with the following content:

```ts title="src/modules/avalara/types.ts" highlights={moduleOptionsHighlights}
export type ModuleOptions = {
  username?: string
  password?: string
  appName?: string
  appVersion?: string
  appEnvironment?: string
  machineName?: string
  timeout?: number
  companyCode?: string
  companyId?: number
}
```

The module options include:

- `username`: The Avalara account ID or username.
- `password`: The Avalara license key or password.
- `appName`: The name of your application. Defaults to `medusa`.
- `appVersion`: The version of your application. Defaults to `1.0.0`.
- `appEnvironment`: The environment of your application, either `production` or `sandbox`. Defaults to `sandbox`.
- `machineName`: The name of the machine where your application is running. Defaults to `medusa`.
- `timeout`: The timeout for API requests in milliseconds. Defaults to `3000`.
- `companyCode`: The Avalara company code to use for tax calculations. If not provided, the default company in Avalara is used.
- `companyId`: The Avalara company ID, which is necessary later when creating items in Avalara.

You'll learn how to set these options when you [register the module](#f-add-module-provider-to-medusas-configuration).

### d. Create Avalara Service

A module has a service that contains its logic. For Tax Module Providers, the service implements the logic to calculate taxes using the third-party service.

To create the service for the Avalara Tax Module Provider, create the file `src/modules/avalara/service.ts` with the following content:

```ts title="src/modules/avalara/service.ts" highlights={serviceHighlights}
import { ITaxProvider } from "@medusajs/framework/types"
import Avatax from "avatax"
import { MedusaError } from "@medusajs/framework/utils"
import { ModuleOptions } from "./types"

type InjectedDependencies = {
  // Add any dependencies you want to inject via the module container
}

class AvalaraTaxModuleProvider implements ITaxProvider {
  static identifier = "avalara"
  private readonly avatax: Avatax
  private readonly options: ModuleOptions

  constructor({}: InjectedDependencies, options: ModuleOptions) {
    this.options = options
    if (!options?.username || !options?.password || !options?.companyId) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Avalara module options are required: username, password and companyId"
      )
    }
    this.avatax = new Avatax({
      appName: options.appName || "medusa",
      appVersion: options.appVersion || "1.0.0",
      machineName: options.machineName || "medusa",
      environment: options.appEnvironment === "production" ? "production" : "sandbox",
      timeout: options.timeout || 3000,
    }).withSecurity({
      username: options.username,
      password: options.password,
    })
  }
  getIdentifier(): string {
    return AvalaraTaxModuleProvider.identifier
  }
}

export default AvalaraTaxModuleProvider
```

A Tax Module Provider's service must implement the `ITaxProvider` interface. It must also have an `identifier` static property with the unique identifier of the provider.

The constructor of a module's service receives the following parameters:

1. An object with the dependencies to resolve from the [Module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md).
2. An object with the module options passed to the provider when it's registered.

In the constructor, you validate that the required options are provided. Then, you create an instance of the `Avatax` client using the provided options.

You also define the `getIdentifier` method required by the `ITaxProvider` interface, which returns the provider's identifier.

#### getTaxLines Method

Next, you'll implement the `getTaxLines` method required by the `ITaxProvider` interface. This method calculates the tax lines for line items and shipping methods. Medusa uses this method during checkout to calculate taxes.

Before creating the method, you'll create a `createTransaction` method that creates a transaction in Avalara to calculate taxes.

First, add the following import at the top of the file:

```ts title="src/modules/avalara/service.ts"
import { CreateTransactionModel } from "avatax/lib/models/CreateTransactionModel"
```

Then, add the following in the `AvalaraTaxModuleProvider` class:

```ts title="src/modules/avalara/service.ts"
class AvalaraTaxModuleProvider implements ITaxProvider {
  // ...
  async createTransaction(model: CreateTransactionModel) {
    try {
      const response = await this.avatax.createTransaction({
        model,
        include: "Details",
      })

      return response
    } catch (error) {
      throw new MedusaError(
        MedusaError.Types.UNEXPECTED_STATE,
        `An error occurred while creating transaction for Avalara: ${error}`
      )
    }
  }
}
```

This method receives the details of the transaction to create in Avalara. It calls the `avatax.createTransaction` method to create the transaction. If the transaction's type ends with `Order`, Avalara will calculate and return the tax details only. If it ends with `Invoice`, Avalara will save the transaction.

You return the response from Avalara, which contains the tax details.

Refer to [Avalara's documentation](https://developer.avalara.com/api-reference/avatax/rest/v2/methods/Transactions/CreateTransaction/) for details on other accepted parameters when creating a transaction.

You'll now add the `getTaxLines` method to calculate tax lines using Avalara.

First, add the following imports at the top of the file:

```ts title="src/modules/avalara/service.ts"
import { 
  ItemTaxCalculationLine, 
  ItemTaxLineDTO, 
  ShippingTaxCalculationLine, 
  ShippingTaxLineDTO, 
  TaxCalculationContext,
} from "@medusajs/framework/types"
```

Then, add the method to the `AvalaraTaxModuleProvider` class:

```ts title="src/modules/avalara/service.ts" highlights={getTaxLinesHighlights}
class AvalaraTaxModuleProvider implements ITaxProvider {
  // ...

  async getTaxLines(
    itemLines: ItemTaxCalculationLine[], 
    shippingLines: ShippingTaxCalculationLine[],
    context: TaxCalculationContext
  ): Promise<(ItemTaxLineDTO | ShippingTaxLineDTO)[]> {
    try {
      const currencyCode = (
        itemLines[0]?.line_item.currency_code || shippingLines[0]?.shipping_line.currency_code
      )?.toUpperCase()
      const response = await this.createTransaction({
        lines: [
          ...(itemLines.length ? itemLines.map((line) => {
            const quantity = Number(line.line_item.quantity) ?? 0
            return {
              number: line.line_item.id,
              quantity,
              amount: quantity * (Number(line.line_item.unit_price) ?? 0),
              taxCode: line.rates.find((rate) => rate.is_default)?.code ?? "",
              itemCode: line.line_item.product_id,
            }
          }) : []),
          ...(shippingLines.length ? shippingLines.map((line) => {
            return {
              number: line.shipping_line.id,
              quantity: 1,
              amount: Number(line.shipping_line.unit_price) ?? 0,
              taxCode: line.rates.find((rate) => rate.is_default)?.code ?? "",
            }
          }) : []),
        ],
        date: new Date(),
        customerCode: context.customer?.id ?? "",
        addresses: {
          "singleLocation": {
            line1: context.address.address_1 ?? "",
            line2: context.address.address_2 ?? "",
            city: context.address.city ?? "",
            region: context.address.province_code ?? "",
            postalCode: context.address.postal_code ?? "",
            country: context.address.country_code.toUpperCase() ?? "",
          },
        },
        currencyCode,
        type: DocumentType.SalesOrder,
      })

      // TODO return tax lines
    } catch (error) {
      throw new MedusaError(
        MedusaError.Types.UNEXPECTED_STATE,
        `An error occurred while getting tax lines from Avalara: ${error}`
      )
    }
  }
}
```

The `getTaxLines` method receives the following parameters:

- `itemLines`: An array of line items to calculate taxes for.
- `shippingLines`: An array of shipping methods to calculate taxes for.
- `context`: Additional context for tax calculation, such as customer and address information.

In the method, you create a transaction in Avalara for both line items and shipping methods using the `avatax.createTransaction` method. Since you set the type to `DocumentType.SalesOrder`, Avalara will calculate and return the tax details for the provided items and shipping methods without saving the transaction.

Refer to [Avalara's documentation](https://developer.avalara.com/api-reference/avatax/rest/v2/methods/Transactions/CreateTransaction/) for details on other accepted parameters when creating a transaction.

Next, you'll extract the tax lines from the response and return them in the expected format. Replace the `// TODO return tax lines` comment with the following:

```ts title="src/modules/avalara/service.ts"
const taxLines: (ItemTaxLineDTO | ShippingTaxLineDTO)[] = []
response?.lines?.forEach((line) => {
  line.details?.forEach((detail) => {
    const isShippingLine = shippingLines.find(
      (sLine) => sLine.shipping_line.id === line.lineNumber
    ) !== undefined
    const commonData = {
      rate: (detail.rate ?? 0) * 100,
      name: detail.taxName ?? "",
      code: line.taxCode || detail.rateTypeCode || detail.signatureCode || "",
      provider_id: this.getIdentifier(),
    }
    if (!isShippingLine) {
      taxLines.push({
        ...commonData,
        line_item_id: line.lineNumber ?? "",
      })
    } else {
      taxLines.push({
        ...commonData,
        shipping_line_id: line.lineNumber ?? "",
      })
    }
  })
})

return taxLines
```

This code extracts the tax details from the Avalara response and constructs an array of tax lines in the expected format. Finally, it returns the array of tax lines.

### e. Export Module Definition

You've finished implementing the Avalara Tax Module Provider's service and its required method.

The final piece of a module is its definition, which you export in an `index.ts` file at the module's root directory. This definition tells Medusa the module's details, including its service.

To create the module's definition, create the file `src/modules/avalara/index.ts` with the following content:

```ts title="src/modules/avalara/index.ts"
import AvalaraTaxModuleProvider from "./service"
import { 
  ModuleProvider, 
  Modules,
} from "@medusajs/framework/utils"

export default ModuleProvider(Modules.TAX, {
  services: [AvalaraTaxModuleProvider],
})
```

You use `ModuleProvider` from the Modules SDK to create the module provider's definition. It accepts two parameters:

1. The name of the module that this provider belongs to, which is `Modules.TAX` in this case.
2. An object with a required `services` property indicating the Module Provider's services.

### f. Add Module Provider to Medusa's Configuration

After finishing the module, add it to Medusa's configuration to start using it.

In `medusa-config.ts`, add a `modules` property:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/tax",
      options: {
        providers: [
          {
            resolve: "./src/modules/avalara",
            id: "avalara",
            options: {
              username: process.env.AVALARA_USERNAME,
              password: process.env.AVALARA_PASSWORD,
              appName: process.env.AVALARA_APP_NAME,
              appVersion: process.env.AVALARA_APP_VERSION,
              appEnvironment: process.env.AVALARA_APP_ENVIRONMENT,
              machineName: process.env.AVALARA_MACHINE_NAME,
              timeout: process.env.AVALARA_TIMEOUT,
              companyCode: process.env.AVALARA_COMPANY_CODE,
              companyId: process.env.AVALARA_COMPANY_ID,
            },
          },
        ],
      },
    },
  ],
})
```

To pass a Tax Module Provider to the Tax Module, add the `modules` property to the Medusa configuration and pass the Tax Module in its value.

The Tax Module accepts a `providers` option, which is an array of Tax Module Providers to register.

You register the Avalara Tax Module Provider and pass it the expected options.

### g. Set Environment Variables

Finally, set the required environment variables in your `.env` file:

```bash title=".env"
AVALARA_USERNAME=
AVALARA_PASSWORD=
AVALARA_APP_ENVIRONMENT=production # or sandbox
AVALARA_COMPANY_ID=
```

You set the following variables:

1. `AVALARA_USERNAME`: Your Avalara account ID. You can retrieve it from the Avalara dashboard by clicking on "Account" at the top right. It's at the top of the dropdown.

![Avalara Account ID in the account dropdown at the top right of the Avalara dashboard](https://res.cloudinary.com/dza7lstvk/image/upload/v1760967142/Medusa%20Resources/CleanShot_2025-10-20_at_16.30.55_2x_gpqi9i.png)

2. `AVALARA_PASSWORD`: Your Avalara license key. To retrieve it from the Avalara dashboard:
   1. From the sidebar, click on "Integrations."
   2. Choose the "License Keys" tab.
   3. Click on the "Generate new key" button.
   4. Confirm generating the key.
   5. Copy the generated key.

![The Avalara license key tab with the "Generate new key" button](https://res.cloudinary.com/dza7lstvk/image/upload/v1760967334/Medusa%20Resources/CleanShot_2025-10-20_at_16.35.06_2x_fe7cym.png)

3. `AVALARA_APP_ENVIRONMENT`: The environment of your application, which can be either `production` or `sandbox`. Set it based on your Avalara account type. Note that Avalara provides separate accounts for production and sandbox environments.
4. `AVALARA_COMPANY_ID`: The company ID in Avalara for creating products for tax code management. To retrieve it from the Avalara dashboard:
   1. From the sidebar, click on "Settings" → "All settings."
   2. Find the "Companies" card and click on "Manage."
   3. Click on the company you want to use.
   4. Copy the company ID from the URL. It's the number at the end of the URL, after `/companies/`.

You can also set other optional environment variables for further configuration. Refer to [Avalara's documentation for more details about these options](https://developer.avalara.com/avatax/client-headers/).

***

## Step 3: Test Tax Calculation with Avalara

You'll test the Avalara integration using the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) that you installed earlier. You'll proceed through checkout and verify that taxes are calculated using Avalara.

### Prerequisite: Set Region's Provider to Avalara

Before testing the integration, configure the regions you want to use Avalara for tax calculations.

First, run the following command in your Medusa application's directory to start the server:

```bash npm2yarn
npm run dev
```

Then:

1. Go to `http://localhost:9000/admin` and log in to the Medusa Admin dashboard.
2. Go to "Settings" → "Tax Regions."
3. Select the country you want to configure Avalara for. You can repeat these steps for multiple countries.
4. In the first section, click on the <InlineIcon Icon={EllipsisHorizontal} alt="three-dots" /> icon at the top right and choose "Edit."
5. In the "Tax Provider" dropdown, select "Avalara (AVALARA)."
6. Click on "Save."

![Setting Avalara as the tax provider for a region in the Medusa Admin dashboard](https://res.cloudinary.com/dza7lstvk/image/upload/v1760968076/Medusa%20Resources/CleanShot_2025-10-20_at_16.47.45_2x_g96acs.png)

### Test Checkout with Avalara

Now you can test the checkout process in the Next.js Starter Storefront.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory name is `{your-project}-storefront`.

If your Medusa application's directory is `medusa-avalara`, find the storefront by going back to the parent directory and changing to the `medusa-avalara-storefront` directory:

```bash
cd ../medusa-avalara-storefront # change based on your project name
```

While the Medusa server is running, open another terminal window in the storefront's directory and run the following command to start the storefront:

```bash npm2yarn
npm run dev
```

Then:

1. Go to `http://localhost:8000` to open the storefront.
2. Go to "Menu" → "Store" and click on a product.
3. Select the product's options if any, then click on "Add to cart."
4. Click on the cart icon at the top right to open the cart.
5. Click on "Go to checkout."
6. Enter the shipping information and click on "Continue to delivery." The tax amount will update in the right section.

![Taxes calculated during checkout in the Next.js Starter Storefront](https://res.cloudinary.com/dza7lstvk/image/upload/v1760968357/Medusa%20Resources/CleanShot_2025-10-20_at_16.52.14_2x_bgozfz.png)

7. In the Delivery step, select a shipping method. This will update the tax amount based on the shipping method's price.

![Taxes updated based on the selected shipping method during checkout in the Next.js Starter Storefront](https://res.cloudinary.com/dza7lstvk/image/upload/v1760968412/Medusa%20Resources/CleanShot_2025-10-20_at_16.53.14_2x_wxcuyn.png)

You can now complete the checkout with the taxes calculated by Avalara.

***

## Step 4: Create Transactions in Avalara on Order Placement

Avalara allows you to create transactions when an order is placed. This helps you keep track of sales and tax liabilities in Avalara.

In this step, you'll implement the logic to create an Avalara transaction when an order is placed in Medusa. You will:

1. Add a method in the Avalara Tax Module Provider's service to uncommit a transaction. This is useful for rolling back the transaction if an error occurs or the order is canceled.
2. Create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) that creates a transaction in Avalara for an order.
3. Create a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that listens to the `order.placed` event and triggers the workflow.

### a. Add Uncommit Transaction Method

First, you'll add a method in the Avalara Tax Module Provider's service to uncommit a transaction.

In `src/modules/avalara/service.ts`, add the following method to the `AvalaraTaxModuleProvider` class:

```ts title="src/modules/avalara/service.ts"
class AvalaraTaxModuleProvider implements ITaxProvider {
  // ...
  async uncommitTransaction(transactionCode: string) {
    try {
      const response = await this.avatax.uncommitTransaction({
        companyCode: this.options.companyCode!,
        transactionCode: transactionCode,
      })

      return response
    }
    catch (error) {
      throw new MedusaError(
        MedusaError.Types.UNEXPECTED_STATE,
        `An error occurred while uncommitting transaction for Avalara: ${error}`
      )
    }
  }
}
```

This method receives the code of the transaction to uncommit. It calls the `avatax.uncommitTransaction` method to uncommit the transaction in Avalara.

### b. Create Order Transaction Workflow

Next, you'll create a workflow that creates an order transaction. A workflow is a series of actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track execution progress, define roll-back logic, and configure other advanced features.

Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

The workflow to create an Avalara transaction has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve order's details.
- [createTransactionStep](#createTransactionStep): Create a transaction for order.
- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md): Save transaction code in order metadata.

Medusa provides the first and last step out of the box. You only need to create the `createTransactionStep`.

#### createTransactionStep

The `createTransactionStep` creates a transaction in Avalara.

To create the step, create the file `src/workflows/steps/create-transaction.ts` with the following content:

```ts title="src/workflows/steps/create-transaction.ts" highlights={createTransactionStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import AvalaraTaxModuleProvider from "../../modules/avalara/service"
import { DocumentType } from "avatax/lib/enums/DocumentType"

type StepInput = {
  lines: {
    number: string
    quantity: number
    amount: number
    taxCode: string
    itemCode?: string
  }[]
  date: Date
  customerCode: string
  addresses: {
    singleLocation: {
      line1: string
      line2: string
      city: string
      region: string
      postalCode: string
      country: string
    }
  }
  currencyCode: string
  type: DocumentType
}

export const createTransactionStep = createStep(
  "create-transaction",
  async (input: StepInput, { container }) => {
    const taxModuleService = container.resolve("tax")
    const avalaraProviderService = taxModuleService.getProvider(
      `tp_${AvalaraTaxModuleProvider.identifier}_avalara`
    ) as AvalaraTaxModuleProvider

    const response = await avalaraProviderService.createTransaction(input)

    return new StepResponse(response, response)
  },
  async (data, { container }) => {
    if (!data?.code) {
      return
    }
    const taxModuleService = container.resolve("tax")
    const avalaraProviderService = taxModuleService.getProvider(
      `tp_${AvalaraTaxModuleProvider.identifier}_avalara`
    ) as AvalaraTaxModuleProvider

    await avalaraProviderService.uncommitTransaction(data.code)
  }
)
```

You create a step with `createStep` from the Workflows SDK. It accepts three parameters:

1. The step's unique name, which is `create-transaction`.
2. An async function that receives two parameters:
   - The step's input, which is an object containing the transaction's details.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.
3. An async compensation function that runs if an error occurs during the workflow's execution. It rolls back changes made in the step.

In the step function, you retrieve the Avalara Tax Module Provider's service from the Tax Module. Then, you call its `createTransaction` method to create a transaction in Avalara.

A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters:

1. The step's output, which is the created transaction.
2. Data to pass to the step's compensation function.

In the compensation function, you uncommit the created transaction if an error occurs during the workflow's execution.

#### Create Transaction Workflow

You'll now create the workflow. Create the file `src/workflows/create-order-transaction.ts` with the following content:

```ts title="src/workflows/create-order-transaction.ts"
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { updateOrderWorkflow, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { createTransactionStep } from "./steps/create-transaction"
import AvalaraTaxModuleProvider from "../modules/avalara/service"
import { DocumentType } from "avatax/lib/enums/DocumentType"

type WorkflowInput = {
  order_id: string
}

export const createOrderTransactionWorkflow = createWorkflow(
  "create-order-transaction-workflow",
  (input: WorkflowInput) => {
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "id",
        "currency_code",
        "items.quantity",
        "items.id",
        "items.unit_price",
        "items.product_id",
        "items.tax_lines.id",
        "items.tax_lines.description",
        "items.tax_lines.code",
        "items.tax_lines.rate",
        "items.tax_lines.provider_id",
        "items.variant.sku",
        "shipping_methods.id",
        "shipping_methods.amount",
        "shipping_methods.tax_lines.id",
        "shipping_methods.tax_lines.description",
        "shipping_methods.tax_lines.code",
        "shipping_methods.tax_lines.rate",
        "shipping_methods.tax_lines.provider_id",
        "shipping_methods.shipping_option_id",
        "customer.id",
        "customer.email",
        "customer.metadata",
        "customer.groups.id",
        "shipping_address.id",
        "shipping_address.address_1",
        "shipping_address.address_2",
        "shipping_address.city",
        "shipping_address.postal_code",
        "shipping_address.country_code",
        "shipping_address.region_code",
        "shipping_address.province",
        "shipping_address.metadata",
      ],
      filters: {
        id: input.order_id,
      },
    })

    // TODO create transaction
  }
)
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

As a second parameter, it accepts a constructor function, which is the workflow's implementation. The function can accept input, which in this case is the order ID.

So far, you retrieve the order's details using the `useQueryGraphStep`. It uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) under the hood to retrieve data across modules.

Next, you'll prepare the transaction input, create the transaction, and update the order with the transaction code. Replace the `// TODO create transaction` comment with the following:

```ts title="src/workflows/create-order-transaction.ts" highlights={createOrderTransactionWorkflowHighlights}
const transactionInput = transform({ orders }, ({ orders }) => {
  const providerId = `tp_${AvalaraTaxModuleProvider.identifier}_avalara`
  return {
    lines: [
      ...(orders[0]?.items?.map((item) => {
        return {
          number: item?.id ?? "",
          quantity: item?.quantity ?? 0,
          amount: item?.unit_price ?? 0,
          taxCode: item?.tax_lines?.find(
            (taxLine) => taxLine?.provider_id === providerId
          )?.code ?? "",
          itemCode: item?.product_id ?? "",
        }
      }) ?? []),
      ...(orders[0]?.shipping_methods?.map((shippingMethod) => {
        return {
          number: shippingMethod?.id ?? "",
          quantity: 1,
          amount: shippingMethod?.amount ?? 0,
          taxCode: shippingMethod?.tax_lines?.find(
            (taxLine) => taxLine?.provider_id === providerId
          )?.code ?? "",
        }
      }) ?? []),
    ],
    date: new Date(),
    customerCode: orders[0]?.customer?.id ?? "",
    addresses: {
      singleLocation: {
        line1: orders[0]?.shipping_address?.address_1 ?? "",
        line2: orders[0]?.shipping_address?.address_2 ?? "",
        city: orders[0]?.shipping_address?.city ?? "",
        region: orders[0]?.shipping_address?.province ?? "",
        postalCode: orders[0]?.shipping_address?.postal_code ?? "",
        country: orders[0]?.shipping_address?.country_code?.toUpperCase() ?? "",
      },
    },
    currencyCode: orders[0]?.currency_code.toUpperCase() ?? "",
    type: DocumentType.SalesInvoice,
  }
})

const response = createTransactionStep(transactionInput)

const order = updateOrderWorkflow.runAsStep({
  input: {
    id: input.order_id,
    user_id: "",
    metadata: {
      avalara_transaction_code: response.code,
    },
  },
})

return new WorkflowResponse(order)
```

You prepare the transaction input using the `transform` function. You include in the input the line items and shipping methods from the order, along with the customer and address details.

Notice that you set the type to `DocumentType.SalesInvoice` to save the transaction in Avalara.

Refer to [Avalara's documentation](https://developer.avalara.com/avatax/client-headers/) for details on other accepted parameters when creating a transaction.

Then, you call the `createTransactionStep` to create the transaction in Avalara.

Finally, you use the `updateOrderWorkflow` to save the created transaction's code in the order's metadata.

A workflow must return an instance of `WorkflowResponse`. The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is the updated order in this case.

`transform` allows you to access the values of data during execution. Learn more in the [Data Manipulation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) documentation.

### c. Create Order Placed Subscriber

Next, you'll create a subscriber that listens to the `order.placed` event and executes the `createOrderTransactionWorkflow` when the event is emitted.

A subscriber is an asynchronous function that is executed when its associated event is emitted.

To create the subscriber, create the file `src/subscribers/order-placed.ts` with the following content:

```ts title="src/subscribers/order-placed.ts"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { createOrderTransactionWorkflow } from "../workflows/create-order-transaction"

export default async function orderPlacedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await createOrderTransactionWorkflow(container).run({
    input: {
      order_id: data.id,
    },
  })
}

export const config: SubscriberConfig = {
  event: `order.placed`,
}
```

A subscriber file must export:

- An asynchronous function that is executed when its associated event is emitted.
- An object that indicates the event that the subscriber is listening to.

The subscriber receives among its parameters the data payload of the emitted event, which includes the order ID.

In the subscriber, you call the `createOrderTransactionWorkflow` with the order ID to create the transaction in Avalara.

### Test Order Placement with Avalara Transaction

To test the order placement with Avalara transaction creation, make sure both the Medusa server and the Next.js Starter Storefront are running.

Then, go to the storefront at `http://localhost:8000` and complete the checkout process you started in the [previous step](#step-3-test-tax-calculation-with-avalara).

You can verify that the transaction was created in Avalara by going to your Avalara dashboard:

1. From the sidebar, click on "Transactions" → "Transactions."
2. In the filter at the top, select "This month to date."
3. In the list, the first transaction should correspond to the order you just placed. Click on it to view its details.

You can view the tax details calculated by Avalara for the order, with the line items and shipping method included in the transaction.

![The transaction's details in the Avalara dashboard showing the calculated taxes for the order](https://res.cloudinary.com/dza7lstvk/image/upload/v1761122370/Medusa%20Resources/CleanShot_2025-10-22_at_11.38.09_2x_bu0jwl.png)

***

## Step 5: Sync Products with Avalara

In Avalara, you can manage the items you sell to set classifications, tax codes, exemptions, and other tax-related settings.

In this step, you'll sync Medusa's products with Avalara items. This way, you can manage tax codes and other settings for your products directly from Avalara.

To do this, you will:

1. Add methods to the Avalara Tax Module Provider's service to manage Avalara items.
2. Build workflows to create, update, and delete Avalara items.
3. Create subscribers that listen to product events and trigger the workflows.

### a. Add Methods to Avalara Tax Module Provider

To manage Avalara items, you'll add methods to the Avalara Tax Module Provider's service that uses the AvaTax API to create, update, and delete items.

In `src/modules/avalara/service.ts`, add the following methods to the `AvalaraTaxModuleProvider` class:

```ts title="src/modules/avalara/service.ts" highlights={avalaraItemMethodsHighlights}
class AvalaraTaxModuleProvider implements ITaxProvider {
  // ...
  async createItems(items: {
    medusaId: string
    itemCode: string
    description: string
    [key: string]: unknown
  }[]) {
    try {
      const response = await this.avatax.createItems({
        companyId: this.options.companyId!,
        model: await Promise.all(
          items.map(async (item) => {
            return {
              ...item,
              id: 0, // Avalara will generate an ID for the item
              itemCode: item.itemCode,
              description: item.description,
              source: "medusa",
              sourceEntityId: item.medusaId,
            }
          })
        ),
      })

      return response
    } catch (error) {
      throw new MedusaError(
        MedusaError.Types.UNEXPECTED_STATE,
        `An error occurred while creating item classifications for Avalara: ${error}`
      )
    }
  }

  async getItem(id: number) {
    try {
      const response = await this.avatax.getItem({
        companyId: this.options.companyId!,
        id,
      })

      return response
    } catch (error) {
      throw new MedusaError(
        MedusaError.Types.UNEXPECTED_STATE,
        `An error occurred while retrieving item classification from Avalara: ${error}`
      )
    }
  }

  async updateItem(item: {
    id: number
    itemCode: string
    description: string
    [key: string]: unknown
  }) {
    try {
      const response = await this.avatax.updateItem({
        companyId: this.options.companyId!,
        id: item.id,
        model: {
          ...item,
          id: item.id,
          itemCode: item.itemCode,
          description: item.description,
          source: "medusa",
        },
      })

      return response
    } catch (error) {
      throw new MedusaError(
        MedusaError.Types.UNEXPECTED_STATE,
        `An error occurred while updating item classifications for Avalara: ${error}`
      )
    }
  }

  async deleteItem(id: number) {
    try {
      const response = await this.avatax.deleteItem({
        companyId: this.options.companyId!,
        id,
      })

      return response
    } catch (error) {
      throw new MedusaError(
        MedusaError.Types.UNEXPECTED_STATE,
        `An error occurred while deleting item classifications for Avalara: ${error}`
      )
    }
  }
}
```

You add the following methods:

1. `createItems`: Creates multiple items in Avalara using their [Create Items API](https://developer.avalara.com/api-reference/avatax/rest/v2/methods/Items/CreateItems/).
2. `getItem`: Retrieves an item from Avalara using their [Get Item API](https://developer.avalara.com/api-reference/avatax/rest/v2/methods/Items/GetItem/).
3. `updateItem`: Updates an item in Avalara using their [Update Item API](https://developer.avalara.com/api-reference/avatax/rest/v2/methods/Items/UpdateItem/).
4. `deleteItem`: Deletes an item in Avalara using their [Delete Item API](https://developer.avalara.com/api-reference/avatax/rest/v2/methods/Items/DeleteItem/).

You'll use these methods in the next sections to build workflows that sync products with Avalara items.

### b. Create Avalara Item Workflow

The first workflow you'll build creates an item for a product in Avalara. It has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve product's details.
- [createItemStep](#createItemStep): Create Avalara item for the product.
- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md): Save Avalara item ID in product metadata.

Medusa provides the first and last step out of the box. You only need to create the `createItemStep`.

#### createItemStep

The `createItemStep` creates an item in Avalara.

To create the step, create the file `src/workflows/steps/create-item.ts` with the following content:

```ts title="src/workflows/steps/create-item.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import AvalaraTaxModuleProvider from "../../modules/avalara/service"

type StepInput = {
  item: {
    medusaId: string
    itemCode: string
    description: string
    [key: string]: unknown
  }
}

export const createItemStep = createStep(
  "create-item",
  async ({ item }: StepInput, { container }) => {
    const taxModuleService = container.resolve("tax")
    const avalaraProviderService = taxModuleService.getProvider(
      `tp_${AvalaraTaxModuleProvider.identifier}_avalara`
    ) as AvalaraTaxModuleProvider

    const response = await avalaraProviderService.createItems(
      [item]
    )

    return new StepResponse(response[0], response[0].id)
  },
  async (data, { container }) => {
    if (!data) {
      return
    }
    const taxModuleService = container.resolve("tax")
    const avalaraProviderService = taxModuleService.getProvider(
      `tp_${AvalaraTaxModuleProvider.identifier}_avalara`
    ) as AvalaraTaxModuleProvider

    avalaraProviderService.deleteItem(data)
  }
)
```

The step receives the details of the item to create as input.

In the step, you retrieve the Avalara Tax Module Provider's service from the Tax Module. Then, you call its `createItems` method to create the item in Avalara.

You return the created item, and you pass its ID to the compensation function to delete the item if an error occurs during the workflow's execution.

Refer to [Avalara's documentation](https://developer.avalara.com/api-reference/avatax/rest/v2/methods/Items/CreateItems/) for details on other accepted parameters when creating an item.

#### Create Product Item Workflow

You can now create the workflow. Create the file `src/workflows/create-product-item.ts` with the following content:

```ts title="src/workflows/create-product-item.ts" highlights={createProductItemWorkflowHighlights}
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { updateProductsWorkflow, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { createItemStep } from "./steps/create-item"

type WorkflowInput = {
  product_id: string
}

export const createProductItemWorkflow = createWorkflow(
  "create-product-item",
  (input: WorkflowInput) => {
    const { data: products } = useQueryGraphStep({
      entity: "product",
      fields: [
        "id",
        "title",
      ],
      filters: {
        id: input.product_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const response = createItemStep({
      item: {
        medusaId: products[0].id,
        itemCode: products[0].id,
        description: products[0].title,
      },
    })

    updateProductsWorkflow.runAsStep({
      input: {
        products: [
          {
            id: input.product_id,
            metadata: {
              avalara_item_id: response.id,
            },
          },
        ],
      },
    })
    
    return new WorkflowResponse(response)
  }
)
```

The workflow receives the product ID as input.

In the workflow, you:

1. Retrieve the product's details using the `useQueryGraphStep`.
2. Create the item in Avalara using the `createItemStep`.
3. Save the created item's ID in the product's metadata using the `updateProductsWorkflow`. This ID is useful when you want to update or delete the item later.

You return the created item as the workflow's output.

### c. Create Product Created Subscriber

Next, you'll create a subscriber that listens to the `product.created` event and executes the `createProductItemWorkflow`.

Create the file `src/subscribers/product-created.ts` with the following content:

```ts title="src/subscribers/product-created.ts"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { createProductItemWorkflow } from "../workflows/create-product-item"

export default async function productCreatedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await createProductItemWorkflow(container).run({
    input: {
      product_id: data.id,
    },
  })
}

export const config: SubscriberConfig = {
  event: `product.created`,
}
```

You create a subscriber similar to the one you created for the `order.placed` event. This time, it listens to the `product.created` event and triggers the `createProductItemWorkflow` with the product ID.

### d. Test Product Creation with Avalara Item

To test the product creation with Avalara item creation, make sure the Medusa server is running.

Then:

1. Open the Medusa Admin dashboard at `http://localhost:9000/admin`.
2. Go to "Products," and create a new product.
3. After creating the product, go to your Avalara dashboard.
4. From the sidebar, click on "Settings" → "What you sell and buy."

This will open the Items page, where you can see the product you created as items in Avalara. You can click on an item to view it, add a classification, and more.

![The list of items in the Avalara dashboard showing the item created for the product](https://res.cloudinary.com/dza7lstvk/image/upload/v1761123470/Medusa%20Resources/CleanShot_2025-10-22_at_11.57.33_2x_jbb4km.png)

### e. Update Avalara Item Workflow

Next, you'll create a workflow that updates a product's item in Avalara. The workflow has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve product's details.

Medusa provides the first step out of the box, and you have already created the `createProductItemWorkflow`. You only need to create the `updateItemStep`.

#### updateItemStep

The `updateItemStep` updates an item in Avalara.

To create the step, create the file `src/workflows/steps/update-item.ts` with the following content:

```ts title="src/workflows/steps/update-item.ts" highlights={updateItemStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import AvalaraTaxModuleProvider from "../../modules/avalara/service"

type StepInput = {
  item: {
    id: number
    medusaId: string
    itemCode: string
    description: string
    [key: string]: unknown
  }
}

export const updateItemStep = createStep(
  "update-item",
  async ({ item }: StepInput, { container }) => {
    const taxModuleService = container.resolve("tax")
    const avalaraProviderService = taxModuleService.getProvider(
      `tp_${AvalaraTaxModuleProvider.identifier}_avalara`
    ) as AvalaraTaxModuleProvider

    // Retrieve original item before updating
    const originalItem = await avalaraProviderService.getItem(item.id)

    // Update the item
    const response = await avalaraProviderService.updateItem(item)

    return new StepResponse(response, {
      originalItem,
    })
  },
  async (data, { container }) => {
    if (!data) {
      return
    }

    const taxModuleService = container.resolve("tax")
    const avalaraProviderService = taxModuleService.getProvider(
      `tp_${AvalaraTaxModuleProvider.identifier}_avalara`
    ) as AvalaraTaxModuleProvider

    // Revert the updates by restoring original values
    await avalaraProviderService.updateItem({
      id: data.originalItem.id,
      itemCode: data.originalItem.itemCode,
      description: data.originalItem.description,
    })
  }
)
```

The step receives the details of the item to update as input.

In the step, you:

1. Retrieve the Avalara Tax Module Provider's service from the Tax Module.
2. Retrieve the original item from Avalara before updating it.
3. Call the `updateItem` method to update the item in Avalara.
4. Return the updated item and pass the original item to the compensation function.

In the compensation function, you revert the updates by restoring the original values if an error occurs during the workflow's execution.

#### Update Product Item Workflow

You can now create the workflow. Create the file `src/workflows/update-product-item.ts` with the following content:

```ts title="src/workflows/update-variant-item.ts" highlights={updateProductItemWorkflowHighlights}
import { createWorkflow, WorkflowResponse, transform, when } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { updateItemStep } from "./steps/update-item"
import { createProductItemWorkflow } from "./create-product-item"

type WorkflowInput = {
  product_id: string
}

export const updateProductItemWorkflow = createWorkflow(
  "update-product-item",
  (input: WorkflowInput) => {
    const { data: products } = useQueryGraphStep({
      entity: "product",
      fields: [
        "id",
        "title",
        "metadata",
      ],
      filters: {
        id: input.product_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const createResponse = when({ products }, ({ products }) => 
      products.length > 0 && !products[0].metadata?.avalara_item_id
    )
      .then(() => {
        return createProductItemWorkflow.runAsStep({
          input: {
            product_id: input.product_id,
          },
        })
      })

    const updateResponse = when({ products }, ({ products }) => 
      products.length > 0 && !!products[0].metadata?.avalara_item_id
    )
      .then(() => {
        return updateItemStep({
          item: {
            id: products[0].metadata?.avalara_item_id as number,
            medusaId: products[0].id,
            itemCode: products[0].id,
            description: products[0].title,
          },
        })
      })

    const response = transform({
      createResponse,
      updateResponse,
    }, (data) => {
      return data.createResponse || data.updateResponse
    })

    return new WorkflowResponse(response)
  }
)
```

The workflow receives the product ID as input.

In the workflow, you:

1. Retrieve the product's details using the `useQueryGraphStep`.
2. Use a `when` condition to check whether the product has an Avalara item ID in its metadata.
   - If it doesn't, you call the `createProductItemWorkflow` to create the item in Avalara.
   - If it does, you prepare the input and call the `updateItemStep` to update the item in Avalara.
3. Use `transform` to return either the created or updated item as the workflow's output.

`when-then` allows you to run steps based on conditions during execution. Learn more in the [Conditions in Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) documentation.

### f. Create Product Updated Subscriber

Next, you'll create a subscriber that listens to the `product.updated` event and executes the `updateProductItemWorkflow`.

Create the file `src/subscribers/product-updated.ts` with the following content:

```ts title="src/subscribers/product-updated.ts"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { updateProductItemWorkflow } from "../workflows/update-product-item"

export default async function productUpdatedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await updateProductItemWorkflow(container).run({
    input: {
      product_id: data.id,
    },
  })
}

export const config: SubscriberConfig = {
  event: `product.updated`,
}


```

You create a subscriber similar to the one you created for the `product.created` event. This time, it listens to the `product.updated` event and triggers the `updateProductItemWorkflow` with the product ID.

### g. Test Product Update with Avalara Item

To test the product update with Avalara item update, make sure the Medusa server is running.

Then:

1. Open the Medusa Admin dashboard at `http://localhost:9000/admin`.
2. Go to "Products," and edit an existing product. For example, you can edit its title.
3. After updating the product, go to your Avalara dashboard.
4. From the sidebar, click on "Settings" → "What you sell and buy."
   - If the product already had an item in Avalara, click on it to view its details and confirm that the changes were applied.
   - If the product didn't have an item in Avalara, you should see a new item created for it.

### h. Delete Avalara Item Workflow

The last workflow you'll build deletes a product's item from Avalara. The workflow has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the product's details.

Medusa provides the first step out of the box. You only need to create the `deleteItemStep`.

#### deleteItemStep

The `deleteItemStep` deletes an item from Avalara.

To create the step, create the file `src/workflows/steps/delete-item.ts` with the following content:

```ts title="src/workflows/steps/delete-item.ts" highlights={deleteItemStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import AvalaraTaxModuleProvider from "../../modules/avalara/service"

type StepInput = {
  item_id: number
}

export const deleteItemStep = createStep(
  "delete-item",
  async ({ item_id }: StepInput, { container }) => {
    const taxModuleService = container.resolve("tax")
    const avalaraProviderService = taxModuleService.getProvider(
      `tp_${AvalaraTaxModuleProvider.identifier}_avalara`
    ) as AvalaraTaxModuleProvider

    try {
      // Retrieve original item before deleting
      const original = await avalaraProviderService.getItem(item_id)
      // Delete the item
      const response = await avalaraProviderService.deleteItem(original.id)

      return new StepResponse(response, {
        originalItem: original,
      })
    } catch (error) {
      console.error(error)
      // Item does not exist in Avalara, so we can skip deletion
      return new StepResponse(void 0)
    }
  },
  async (data, { container }) => {
    if (!data) {
      return
    }

    const taxModuleService = container.resolve("tax")
    const avalaraProviderService = taxModuleService.getProvider(
      `tp_${AvalaraTaxModuleProvider.identifier}_avalara`
    ) as AvalaraTaxModuleProvider

    await avalaraProviderService.createItems(
      [{
        medusaId: data.originalItem.sourceEntityId ?? "",
        description: data.originalItem.description,
        itemCode: data.originalItem.itemCode,
      }]
    )
  }
)
```

The step receives the ID of the item to delete as input.

In the step, you:

1. Retrieve the Avalara Tax Module Provider's service from the Tax Module.
2. Retrieve the original item before deleting it.
3. Call the `deleteItem` method to delete the item in Avalara.
4. Return the deletion response and pass the original item to the compensation function.

In the compensation function, you recreate the item using the original values if an error occurs during the workflow's execution.

#### Delete Product Item Workflow

You can now create the workflow. Create the file `src/workflows/delete-product-item.ts` with the following content:

```ts title="src/workflows/delete-product-item.ts" highlights={deleteProductItemWorkflowHighlights}
import { createWorkflow, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { deleteItemStep } from "./steps/delete-item"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

type WorkflowInput = {
  product_id: string
}

export const deleteProductItemWorkflow = createWorkflow(
  "delete-product-item",
  (input: WorkflowInput) => {
    const { data: products } = useQueryGraphStep({
      entity: "product",
      fields: [
        "id",
        "metadata",
      ],
      filters: {
        id: input.product_id,
      },
      withDeleted: true,
      options: {
        throwIfKeyNotFound: true,
      },
    })

    when({ products }, ({ products }) =>
      products.length > 0 && !!products[0].metadata?.avalara_item_id
    )
    .then(() => {
      deleteItemStep({ item_id: products[0].metadata!.avalara_item_id as number })
    })

    return new WorkflowResponse(void 0)
  }
)
```

The workflow receives the product ID as input.

In the workflow, you:

1. Retrieve the product's details using the `useQueryGraphStep`, including deleted products.
2. Use a `when` condition to check whether the product has an Avalara item ID in its metadata.
   - If it does, you call the `deleteItemStep` to delete the item in Avalara.

### i. Create Product Deleted Subscriber

Finally, you'll create a subscriber to delete the Avalara item when a product is deleted.

Create the file `src/subscribers/product-deleted.ts` with the following content:

```ts title="src/subscribers/product-deleted.ts"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { deleteProductItemWorkflow } from "../workflows/delete-product-item"

export default async function productDeletedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await deleteProductItemWorkflow(container).run({
    input: {
      product_id: data.id,
    },
    throwOnError: false,
  })
}

export const config: SubscriberConfig = {
  event: `product.deleted`,
}


```

You create a subscriber similar to the previous ones. This time, the subscriber listens to the `product.deleted` event and triggers the `deleteProductItemWorkflow` with the product ID.

### j. Test Product Deletion with Avalara Item

To test the product deletion with Avalara item deletion, make sure the Medusa server is running.

Then:

1. Open the Medusa Admin dashboard at `http://localhost:9000/admin`.
2. Go to "Products," and delete a product.
3. After deleting the product, go to your Avalara dashboard.
4. From the sidebar, click on "Settings" → "What you sell and buy." The product's item should no longer be listed.

***

## Next Steps

You've successfully integrated Avalara with Medusa to handle tax calculations during checkout, create transactions when orders are placed, and sync products with Avalara items.

You can expand on this integration by managing tax features in Avalara, such as exemptions. You can also handle order events like `order.canceled` to void transactions in Avalara when orders are canceled.

### Learn More About Medusa

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md) for a more in-depth understanding of the concepts used in this guide.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.


# Implement Localization in Medusa by Integrating Contentful

In this tutorial, you'll learn how to localize your Medusa store's data with Contentful.

[Translation Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/translation/index.html.md) is available since [Medusa v2.12.3](https://github.com/medusajs/medusa/releases/tag/v2.12.3). It allows you to manage translations for product-related resources through Medusa's Store API routes. For general localization needs, consider using the Translation Module instead of integrating a third-party CMS.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. While Medusa provides features essential for internationalization, such as support for multiple [regions](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md) and [currencies](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/index.html.md), it doesn't provide content localization.

However, Medusa's architecture supports the integration of third-party services to provide additional features, such as data localization. One service you can integrate is [Contentful](https://www.contentful.com/), a headless content management system (CMS) that allows you to manage and deliver content across multiple channels.

## Summary

By following this tutorial, you'll learn how to:

- Install and set up Medusa.
- Integrate Contentful with Medusa.
- Create content types in Contentful for Medusa models.
- Trigger syncing products and related data to Contentful when:
  - A product is created.
  - The admin user triggers syncing the products.
- Customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to fetch localized data from Contentful through Medusa.
- Listen to webhook events in Contentful to update Medusa's data accordingly.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Diagram illustrating the integration of Contentful with Medusa](https://res.cloudinary.com/dza7lstvk/image/upload/v1744791908/Medusa%20Resources/contentful-summary_j5cpdx.jpg)

- [Tutorial Repository](https://github.com/medusajs/examples/tree/main/localization-contentful): Find the full code for this guide in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1744790686/OpenApi/Contentful_jysc07.yaml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

First, you'll be asked for the project's name. Then, when prompted about installing the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Contentful Module

To integrate third-party services into Medusa, you create a module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a reusable package that provides functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

In this step, you'll create a module that provides the necessary functionalities to integrate Contentful with Medusa.

Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules and their structure.

### Install Contentful SDKs

Before building the module, you need to install Contentful's management and delivery JS SDKs. So, run the following command in the Medusa application's directory:

```bash npm2yarn
npm install contentful contentful-management
```

Where `contentful` is the delivery SDK and `contentful-management` is the management SDK.

### Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/contentful`.

### Create Loader

When the Medusa application starts, you want to establish a connection to Contentful, then create the necessary content types if they don't exist in Contentful.

A module can specify a task to run on the Medusa application's startup using [loaders](https://docs.medusajs.com/docs/learn/fundamentals/modules/loaders/index.html.md). A loader is an asynchronous function that a module exports. Then, when the Medusa application starts, it runs the loader. The loader can be used to perform one-time tasks such as connecting to a database, creating content types, or initializing data.

Refer to the [Loaders](https://docs.medusajs.com/docs/learn/fundamentals/modules/loaders/index.html.md) documentation to learn more about how loaders work and when to use them.

Loaders are created in a TypeScript or JavaScript file under the `loaders` directory of a module. So, create the file `src/modules/contentful/loader/create-content-models.ts` with the following content:

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

const { createClient: createDeliveryClient } = require("contentful")

export type ModuleOptions = {
  management_access_token: string
  delivery_token: string
  space_id: string
  environment: string
  default_locale?: string
}

export default async function syncContentModelsLoader({
  container,
  options,
}: LoaderOptions<ModuleOptions>) {
  if (
    !options?.management_access_token || !options?.delivery_token || 
    !options?.space_id || !options?.environment
  ) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "Contentful access token, space ID and environment are required"
    )
  }

  const logger = container.resolve("logger")

  try {
    const managementClient = createClient({
      accessToken: options.management_access_token,
    }, {
      type: "plain",
      defaults: {
        spaceId: options.space_id,
        environmentId: options.environment,
      },
    })

    const deliveryClient = createDeliveryClient({
      accessToken: options.delivery_token,
      space: options.space_id,
      environment: options.environment,
    })


    // TODO try to create content types

  } catch (error) {
    logger.error(
      `Failed to connect to Contentful: ${error}`
    )
    throw error
  }
}
```

The loader file exports an asynchronous function that accepts an object having the following properties:

- `container`: The [Module container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md), which is a registry of resources available to the module. You can use it to resolve or register resources in the module's container.
- `options`: An object of options passed to the module. These options are useful to pass secrets or options that may change per environment. You'll learn how to pass these options later.
  - The Contentful Module expects the options to include the Contentful tokens for the management and delivery APIs, the space ID, environment, and optionally the default locale to use.

In the loader function, you validate the options passed to the module, and throw an error if they're invalid. Then, you resolve from the Module's container the [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) used to log messages in the terminal.

Finally, you create clients for Contentful's management and delivery APIs, passing them the necessary module's options. If the connection fails, an error is thrown, which is handled in the `catch` block.

#### Create Content Types

In the loader, you need to create content types in Contentful if they don't already exist.

In this tutorial, you'll only create content types for a product and its variants and options. However, you can create content types for other data models, such as categories or collections, by following the same approach.

You can learn more about the product-related data models, which the content types are based on, in the [Product Module's Data Models](https://docs.medusajs.com/references/product/models/index.html.md) reference.

To create the content type for products, replace the `TODO` in the loader with the following:

```ts title="src/modules/contentful/loader/create-content-models.ts"
// Try to create the product content type
try {
  await managementClient.contentType.get({
    contentTypeId: "product",
  })
} catch (error) {
  const productContentType = await managementClient.contentType.createWithId({
    contentTypeId: "product",
  }, {
    name: "Product",
    description: "Product content type synced from Medusa",
    displayField: "title",
    fields: [
      {
        id: "title", 
        name: "Title",
        type: "Symbol",
        required: true,
        localized: true,
      },
      {
        id: "handle",
        name: "Handle", 
        type: "Symbol",
        required: true,
        localized: false,
      },
      {
        id: "medusaId",
        name: "Medusa ID",
        type: "Symbol",
        required: true,
        localized: false,
      },
      {
        type: "RichText",
        name: "description", 
        id: "description",
        validations: [
          {
            enabledMarks: [
              "bold",
              "italic",
              "underline", 
              "code",
              "superscript",
              "subscript",
              "strikethrough",
            ],
          },
          {
            enabledNodeTypes: [
              "heading-1",
              "heading-2", 
              "heading-3",
              "heading-4",
              "heading-5",
              "heading-6",
              "ordered-list",
              "unordered-list",
              "hr",
              "blockquote",
              "embedded-entry-block",
              "embedded-asset-block",
              "table",
              "asset-hyperlink",
              "embedded-entry-inline",
              "entry-hyperlink",
              "hyperlink",
            ],
          },
          {
            nodes: {},
          },
        ],
        localized: true,
        required: true,
      },
      {
        type: "Symbol",
        name: "subtitle",
        id: "subtitle",
        localized: true,
        required: false,
        validations: [],
      },
      {
        type: "Array",
        items: {
          type: "Link",
          linkType: "Asset",
          validations: [],
        },
        name: "images",
        id: "images",
        localized: true,
        required: false,
        validations: [],
      },
      {
        id: "productVariants",
        name: "Product Variants",
        type: "Array",
        localized: false,
        required: false,
        items: {
          type: "Link",
          validations: [
            {
              linkContentType: ["productVariant"],
            },
          ],
          linkType: "Entry",
        },
        disabled: false,
        omitted: false,
      },
      {
        id: "productOptions",
        name: "Product Options",
        type: "Array",
        localized: false,
        required: false,
        items: {
          type: "Link",
          validations: [
            {
              linkContentType: ["productOption"],
            },
          ],
          linkType: "Entry",
        },
        disabled: false,
        omitted: false,
      },
    ],
  })

  await managementClient.contentType.publish({
    contentTypeId: "product",
  }, productContentType)
}

// TODO create product variant content type
```

In the above snippet, you first try to retrieve the product content type using Contentful's Management APIs. If the content type doesn't exist, an error is thrown, which you handle in the `catch` block.

In the `catch` block, you create the product content type with the following fields:

- `title`: The product's title, which is a localized field.
- `handle`: The product's handle, which is used to create a human-readable URL for the product in the storefront.
- `medusaId`: The product's ID in Medusa, which is a non-localized field. You'll store in this field the ID of the product in Medusa.
- `description`: The product's description, which is a localized rich-text field.
- `subtitle`: The product's subtitle, which is a localized field.
- `images`: The product's images, which is a localized array of assets in Contentful.
- `productVariants`: The product's variants, which is an array that references content of the `productVariant` content type.
- `productOptions`: The product's options, which is an array that references content of the `productOption` content type.

Next, you'll create the `productVariant` content type that represents a product's variant. A variant is a combination of the product's options that customers can purchase. For example, a "red" shirt is a variant whose color option is `red`.

To create the variant content type, replace the new `TODO` with the following:

```ts title="src/modules/contentful/loader/create-content-models.ts"
// Try to create the product variant content type
try {
  await managementClient.contentType.get({
    contentTypeId: "productVariant",
  })
} catch (error) {
  const productVariantContentType = await managementClient.contentType.createWithId({
    contentTypeId: "productVariant",
  }, {
  name: "Product Variant",
  description: "Product variant content type synced from Medusa",
  displayField: "title",
  fields: [
    {
      id: "title",
      name: "Title",
      type: "Symbol",
      required: true,
      localized: true,
    },
    {
      id: "product",
      name: "Product",
      type: "Link",
      required: true,
      localized: false,
      validations: [
        {
          linkContentType: ["product"],
        },
      ],
      disabled: false,
      omitted: false,
      linkType: "Entry",
    },
    {
      id: "medusaId",
      name: "Medusa ID",
      type: "Symbol",
      required: true,
      localized: false,
    },
    {
      id: "productOptionValues",
      name: "Product Option Values",
      type: "Array",
      localized: false,
      required: false,
      items: {
        type: "Link",
        validations: [
          {
            linkContentType: ["productOptionValue"],
          },
        ],
        linkType: "Entry",
      },
      disabled: false,
      omitted: false,
      },
    ],
  })

  await managementClient.contentType.publish({
    contentTypeId: "productVariant",
  }, productVariantContentType)
}

// TODO create product option content type
```

In the above snippet, you create the `productVariant` content type with the following fields:

- `title`: The product variant's title, which is a localized field.
- `product`: References the `product` content type, which is the product that the variant belongs to.
- `medusaId`: The product variant's ID in Medusa, which is a non-localized field. You'll store in this field the ID of the variant in Medusa.
- `productOptionValues`: The product variant's option values, which is an array that references content of the `productOptionValue` content type.

Then, you'll create the `productOption` content type that represents a product's option, like size or color. Replace the new `TODO` with the following:

```ts title="src/modules/contentful/loader/create-content-models.ts"
// Try to create the product option content type
try {
  await managementClient.contentType.get({
    contentTypeId: "productOption",
  })
} catch (error) {
  const productOptionContentType = await managementClient.contentType.createWithId({
    contentTypeId: "productOption",
  }, {
    name: "Product Option",
    description: "Product option content type synced from Medusa",
    displayField: "title",
    fields: [
    {
      id: "title",
      name: "Title",
      type: "Symbol",
      required: true,
      localized: true,
    },
    {
      id: "product",
      name: "Product",
      type: "Link",
      required: true,
      localized: false,
      validations: [
        {
          linkContentType: ["product"],
        },
      ],
      disabled: false,
      omitted: false,
      linkType: "Entry",
    },
    {
      id: "medusaId",
      name: "Medusa ID",
      type: "Symbol",
      required: true,
      localized: false,
    },
    {
      id: "values",
      name: "Values",
      type: "Array",
      required: false,
      localized: false,
      items: {
        type: "Link",
        validations: [
          {
            linkContentType: ["productOptionValue"],
          },
        ],
        linkType: "Entry",
      },
      disabled: false,
      omitted: false,
      },
    ],
  })

  await managementClient.contentType.publish({
    contentTypeId: "productOption",
  }, productOptionContentType)
}

// TODO create product option value content type
```

In the above snippet, you create the `productOption` content type with the following fields:

- `title`: The product option's title, which is a localized field.
- `product`: References the `product` content type, which is the product that the option belongs to.
- `medusaId`: The product option's ID in Medusa, which is a non-localized field. You'll store in this field the ID of the option in Medusa.
- `values`: The product option's values, which is an array that references content of the `productOptionValue` content type.

Finally, you'll create the `productOptionValue` content type that represents a product's option value, like "red" or "blue" for the color option. A variant references option values.

To create the option value content type, replace the new `TODO` with the following:

```ts title="src/modules/contentful/loader/create-content-models.ts"
// Try to create the product option value content type
try {
  await managementClient.contentType.get({
    contentTypeId: "productOptionValue",
  })
} catch (error) {
  const productOptionValueContentType = await managementClient.contentType.createWithId({
    contentTypeId: "productOptionValue",
  }, {
    name: "Product Option Value",
    description: "Product option value content type synced from Medusa",
    displayField: "value",
    fields: [
    {
      id: "value",
      name: "Value",
      type: "Symbol",
      required: true,
      localized: true,
    },
    {
      id: "medusaId",
      name: "Medusa ID",
      type: "Symbol",
      required: true,
      localized: false,
    },
  ],
})

await managementClient.contentType.publish({
    contentTypeId: "productOptionValue",
  }, productOptionValueContentType)
}

// TODO register clients in container
```

In the above snippet, you create the `productOptionValue` content type with the following fields:

- `value`: The product option value, which is a localized field.
- `medusaId`: The product option value's ID in Medusa, which is a non-localized field. You'll store in this field the ID of the option value in Medusa.

You've now created all the necessary content types to localize products.

### Register Clients in the Container

The last step in the loader is to register the Contentful management and delivery clients in the module's container. This will allow you to resolve and use them in the module's service, which you'll create next.

To register resources in the container, you can use its `register` method, which accepts an object containing key-value pairs. The keys are the names of the resources in the container, and the values are the resources themselves.

To register the management and delivery clients, replace the last `TODO` in the loader with the following:

```ts title="src/modules/contentful/loader/create-content-models.ts"
container.register({
  contentfulManagementClient: asValue(managementClient),
  contentfulDeliveryClient: asValue(deliveryClient),
})

logger.info("Connected to Contentful")
```

Now, you can resolve the management and delivery clients from the module's container using the keys `contentfulManagementClient` and `contentfulDeliveryClient`, respectively.

### Create Service

You define a module's functionality in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, which is useful if your module defines tables in the database, or perform actions with a third-party service.

In this section, you'll create the Contentful Module's service that can be used to retrieve content from Contentful, create content, and more.

To create the service, create the file `src/modules/contenful/service.ts` with the following content:

```ts title="src/modules/contentful/service.ts" highlights={serviceHighlights}
import { ModuleOptions } from "./loader/create-content-models"
import { PlainClientAPI } from "contentful-management"

type InjectedDependencies = {
  contentfulManagementClient: PlainClientAPI;
  contentfulDeliveryClient: any;
}

export default class ContentfulModuleService {
  private managementClient: PlainClientAPI
  private deliveryClient: any
  private options: ModuleOptions

  constructor(
    { 
      contentfulManagementClient, 
      contentfulDeliveryClient,
    }: InjectedDependencies, 
    options: ModuleOptions
  ) {
    this.managementClient = contentfulManagementClient
    this.deliveryClient = contentfulDeliveryClient
    this.options = {
      ...options,
      default_locale: options.default_locale || "en-US",
    }
  }

  // TODO add methods
}
```

You export a class that will be the Contentful Module's main service. In the class, you define properties for the Contentful clients and options passed to the module.

You also add a constructor to the class. A service's constructor accepts the following params:

1. The module's container, which you can use to resolve resources. You use it to resolve the Contentful clients you previously registered in the loader.
2. The options passed to the module.

In the constructor, you assign the clients and options to the class properties. You also set the default locale to `en-US` if it's not provided in the module's options.

Since the loader is executed on application start-up, if an error occurs while connecting to Contentful, the module will not be registered and the service will not be executed. So, in the service, you're guaranteed that the clients are registered in the container and have successful connection to Contentful.

As you implement the syncing and content retrieval features later, you'll add the necessary methods for them.

### Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at the module's root directory. This definition tells Medusa the name of the module, its service, and optionally its loaders.

To create the module's definition, create the file `src/modules/contentful/index.ts` with the following content:

```ts title="src/modules/contentful/index.ts" highlights={moduleHighlights}
import { Module } from "@medusajs/framework/utils"
import ContentfulModuleService from "./service"
import createContentModelsLoader from "./loader/create-content-models"

export const CONTENTFUL_MODULE = "contentful"

export default Module(CONTENTFUL_MODULE, {
  service: ContentfulModuleService,
  loaders: [
    createContentModelsLoader,
  ],
})
```

You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `contentful`.
2. An object with a required property `service` indicating the module's service. You also pass the loader you created to ensure it's executed when the application starts.

Aside from the module definition, you export the module's name as `CONTENTFUL_MODULE` so you can reference it later.

### Add Module to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/contentful",
      options: {
        management_access_token: process.env.CONTENTFUL_MANAGEMNT_ACCESS_TOKEN,
        delivery_token: process.env.CONTENTFUL_DELIVERY_TOKEN,
        space_id: process.env.CONTENTFUL_SPACE_ID,
        environment: process.env.CONTENTFUL_ENVIRONMENT,
        default_locale: "en-US",
      },
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package's name.

You also pass an `options` property with the module's options, including the Contentful's tokens for the management and delivery APIs, the Contentful's space ID, environment, and default locale.

### Note about Locales

By default, your Contentful space will have one locale (for example, `en-US`). You can add locales as explained in the [Contentful documentation](https://www.contentful.com/help/localization/manage-locales/).

When you add a locale, make sure to:

- Set the fallback locale to the default locale (for example, `en-US`). This ensure that values are retrieved in the default locale if values for the requested locale are not available.
- Allow the required fields to be empty for the locale. Otherwise, you'll have to specify the values for the localized fields in each locale when you create the products later.

![Example of the locale's settings in the Contentful dashboard](https://res.cloudinary.com/dza7lstvk/image/upload/v1744716537/Medusa%20Resources/Screenshot_2025-04-15_at_2.28.46_PM_v50lrm.png)

### Add Environment Variables

Before you can start using the Contentful Module, you need to add the necessary environment variables used in the module's options.

Add the following environment variables to your `.env` file:

```plain
CONTENTFUL_MANAGEMNT_ACCESS_TOKEN=CFPAT-...
CONTENTFUL_DELIVERY_TOKEN=eij...
CONTENTFUL_SPACE_ID=t2a...
CONTENTFUL_ENVIRONMENT=master
```

Where:

- `CONTENTFUL_MANAGEMNT_ACCESS_TOKEN`: The Contentful management API access token. To create it on the Contentful dashboard:
  - Click on the cog icon at the top right, then choose "CMA tokens" from the dropdown.

![The cog icon is at the top right next to the user avatar. If you click on it, a dropdown will show where you can click on "CMA tokens".](https://res.cloudinary.com/dza7lstvk/image/upload/v1744714244/Medusa%20Resources/Screenshot_2025-04-15_at_1.48.35_PM_ct2tfk.png)

- In the CMA tokens page, click on the "Create personal access token" button.
- In the window that pops up, enter a name for the token, and choose an expiry date. Once you're done, click the Generate button.
- The token is generated and shown in the pop-up. Make sure to copy it and use it in the `.env` file, as you can't access it again.

![Click on the copy button to copy the key](https://res.cloudinary.com/dza7lstvk/image/upload/v1744714749/Medusa%20Resources/Screenshot_2025-04-15_at_1.58.12_PM_tuugam.png)

- `CONTENTFUL_DELIVERY_TOKEN`: An API token that you can use with the delivery API. To create it on the Contentful dashboard:
  - Click on the cog icon at the top right, then choose "API keys" from the dropdown.

![The cog icon is at the top right next to the user avatar. If you click on it, a dropdown will show where you can click on "API keys".](https://res.cloudinary.com/dza7lstvk/image/upload/v1744714971/Medusa%20Resources/Screenshot_2025-04-15_at_2.02.31_PM_qfsn1h.png)

- In the APIs page, click on the "Add API key" button.
- In the window that pops up, enter a name for the token, then click the Add API Key button.
- This will create an API key and opens its page. On its page, copy the token for the "Content Delivery API" and use it as the value for `CONTENTFUL_DELIVERY_TOKEN`.

![Copy the API key from the "Content Delivery API - access token" field](https://res.cloudinary.com/dza7lstvk/image/upload/v1744715228/Medusa%20Resources/Screenshot_2025-04-15_at_2.06.44_PM_ifu1mx.png)

- `CONTENTFUL_SPACE_ID`: The ID of your Contentful space. You can copy this from the dashboard's URL which is of the format `https://app.contentful.com/spaces/{space_id}/...`.
- `CONTENTFUL_ENVIRONMENT`: The environment to manage and retrieve the content in. By default, you have the `master` environment which you can use. However, you can use another Contentful environment that you've created.

Your module is now ready for use.

### Test the Module

To test out the module, you'll start the Medusa application, which will run the module's loader.

To start the Medusa application, run the following command:

```bash npm2yarn
npm run dev
```

If the loader ran successfully, you'll see the following message in the terminal:

```bash
info:    Connected to Contentful
```

You can also see on the Contentful dashboard that the content types were created. To view them, go to the Content Model page.

![In the Contentful dashboard, go to the Content Model page](https://res.cloudinary.com/dza7lstvk/image/upload/v1744715783/Medusa%20Resources/Screenshot_2025-04-15_at_2.16.09_PM_w7oszm.png)

***

## Step 3: Create Products in Contentful

Now that you have the Contentful Module ready for use, you can start creating products in Contentful.

In this step, you'll implement the logic to create products in Contentful. Later, you'll execute it when:

- A product is created in Medusa.
- The admin user triggers a sync manually.

### Add Methods to Contentful Module Service

To create products in Contentful, you need to add the necessary methods in the Contentful Module's service. Then, you can use these methods later when building the creation flow.

To create a product in Contentful, you'll need three methods: One to create the product's variants, another to create the product's options and values, and a third to create the product.

In the service at `src/modules/contentful/service.ts`, start by adding the method to create the product's variants:

```ts title="src/modules/contentful/service.ts"
// imports...
import { ProductVariantDTO } from "@medusajs/framework/types"
import { EntryProps } from "contentful-management"

export default class ContentfulModuleService {
  // ...

  private async createProductVariant(
    variants: ProductVariantDTO[],
    productEntry: EntryProps
  ) {
    for (const variant of variants) {
      await this.managementClient.entry.createWithId(
        {
          contentTypeId: "productVariant",
          entryId: variant.id,
        },
        {
          fields: {
            medusaId: {
              [this.options.default_locale!]: variant.id,
            },
            title: {
              [this.options.default_locale!]: variant.title,
            },
            product: {
              [this.options.default_locale!]: {
                sys: {
                  type: "Link",
                  linkType: "Entry",
                  id: productEntry.sys.id,
                },
              },
            },
            productOptionValues: {
              [this.options.default_locale!]: variant.options.map((option) => ({
                sys: {
                  type: "Link",
                  linkType: "Entry",
                  id: option.id,
                },
              })),
            },
          },
        }
      )
    }
  }
}
```

You define a private method `createProductVariant` that accepts two parameters:

1. The product's variants to create in Contentful.
2. The product's entry in Contentful.

In the method, you iterate over the product's variants and create a new entry in Contentful for each variant. You set the fields based on the product variant content type you created earlier.

For each field, you specify the value for the default locale. In the Contentful dashboard, you can manage the values for other locales.

Next, add the method to create the product's options and values:

```ts title="src/modules/contentful/service.ts" highlights={createProductOptionHighlights}
// other imports...
import { ProductOptionDTO } from "@medusajs/framework/types"

export default class ContentfulModuleService {
  // ...
  private async createProductOption(
    options: ProductOptionDTO[],
    productEntry: EntryProps
  ) {
    for (const option of options) {
      const valueIds: {
        sys: {
          type: "Link",
          linkType: "Entry",
          id: string
        }
      }[] = []
      for (const value of option.values) {
        await this.managementClient.entry.createWithId(
          {
            contentTypeId: "productOptionValue",
            entryId: value.id,
          },
          {
            fields: {
              value: {
                [this.options.default_locale!]: value.value,
              },
              medusaId: {
                [this.options.default_locale!]: value.id,
              },
            },
          }
        )
        valueIds.push({
          sys: {
            type: "Link",
            linkType: "Entry",
            id: value.id,
          },
        })
      }
      await this.managementClient.entry.createWithId(
        {
          contentTypeId: "productOption",
          entryId: option.id,
        },
        {
          fields: {
            medusaId: {
              [this.options.default_locale!]: option.id,
            },
            title: {
              [this.options.default_locale!]: option.title,
            },
            product: {
              [this.options.default_locale!]: {
                sys: {
                  type: "Link",
                  linkType: "Entry",
                  id: productEntry.sys.id,
                },
              },
            },
            values: {
              [this.options.default_locale!]: valueIds,
            },
          },
        }
      )
    }
  }
}
```

You define a private method `createProductOption` that accepts two parameters:

1. The product's options, which is an array of objects.
2. The product's entry in Contentful, which is an object.

In the method, you iterate over the product's options and create entries for each of its values. Then, you create an entry for the option, and reference the values you created in Contentful. You set the fields based on the option and value content types you created earlier.

Finally, add the method to create the product:

```ts title="src/modules/contentful/service.ts" highlights={createProductHighlights}
// other imports...
import { ProductDTO } from "@medusajs/framework/types"

export default class ContentfulModuleService {
  // ...
  async createProduct(
    product: ProductDTO
  ) {
    try {
      // check if product already exists
      const productEntry = await this.managementClient.entry.get({
        environmentId: this.options.environment,
        entryId: product.id,
      })
      
      return productEntry
    } catch(e) {}
    
    // Create product entry in Contentful
    const productEntry = await this.managementClient.entry.createWithId(
      {
        contentTypeId: "product",
        entryId: product.id,
      },
      {
        fields: {
          medusaId: {
            [this.options.default_locale!]: product.id,
          },
          title: {
            [this.options.default_locale!]: product.title,
          },
          description: product.description ? {
            [this.options.default_locale!]: {
              nodeType: "document",
              data: {},
              content: [
                {
                  nodeType: "paragraph",
                  data: {},
                  content: [
                    {
                      nodeType: "text",
                      value: product.description,
                      marks: [],
                      data: {},
                    },
                  ],
                },
              ],
            },
          } : undefined,
          subtitle: product.subtitle ? {
            [this.options.default_locale!]: product.subtitle,
          } : undefined,
          handle: product.handle ? {
            [this.options.default_locale!]: product.handle,
          } : undefined,
        },
      }
    )

    // Create options if they exist
    if (product.options?.length) {
      await this.createProductOption(product.options, productEntry)
    }

    // Create variants if they exist
    if (product.variants?.length) {
      await this.createProductVariant(product.variants, productEntry)
    }

    // update product entry with variants and options
    await this.managementClient.entry.update(
      {
        entryId: productEntry.sys.id,
      },
      {
        sys: productEntry.sys,
        fields: {
          ...productEntry.fields,
          productVariants: {
            [this.options.default_locale!]: product.variants?.map((variant) => ({
              sys: {
                type: "Link",
                linkType: "Entry",
                id: variant.id,
              },
            })),
          },
          productOptions: {
            [this.options.default_locale!]: product.options?.map((option) => ({
              sys: {
                type: "Link",
                linkType: "Entry",
                id: option.id,
              },
            })),
          },
        },
      }
    )

    return productEntry
  }
}
```

You define a public method `createProduct` that accepts a product object as a parameter.

In the method, you first check if the product already exists in Contentful. If it does, you return the existing product entry. Otherwise, you create a new product entry with the fields based on the product content type you created earlier.

Next, you create entries for the product's options and variants using the methods you created earlier.

Finally, you update the product entry to reference the variants and options you created.

You now have all the methods to create products in Contentful. You'll also need one last method to delete a product in Contentful. This is useful when you implement the rollback mechanism in the flow that creates the products.

Add the following method to the service:

```ts title="src/modules/contentful/service.ts" highlights={deleteProductHighlights}
// other imports...
import { MedusaError } from "@medusajs/framework/utils"

export default class ContentfulModuleService {
  // ...
  async deleteProduct(productId: string) {
    try {
      // Get the product entry
      const productEntry = await this.managementClient.entry.get({
        environmentId: this.options.environment,
        entryId: productId,
      })

      if (!productEntry) {
        return
      }

      // Delete the product entry
      await this.managementClient.entry.unpublish({
        environmentId: this.options.environment,
        entryId: productId,
      })

      await this.managementClient.entry.delete({
        environmentId: this.options.environment,
        entryId: productId,
      })

      // Delete the product variant entries
      for (const variant of productEntry.fields.productVariants[this.options.default_locale!]) {
        await this.managementClient.entry.unpublish({
          environmentId: this.options.environment,
          entryId: variant.sys.id,
        })

        await this.managementClient.entry.delete({
          environmentId: this.options.environment,
          entryId: variant.sys.id,
        })
      }

      // Delete the product options entries and values
      for (const option of productEntry.fields.productOptions[this.options.default_locale!]) {
        for (const value of option.fields.values[this.options.default_locale!]) {
          await this.managementClient.entry.unpublish({
            environmentId: this.options.environment,
            entryId: value.sys.id,
        })

        await this.managementClient.entry.delete({
          environmentId: this.options.environment,
            entryId: value.sys.id,
          })
        }

        await this.managementClient.entry.unpublish({
          environmentId: this.options.environment,
          entryId: option.sys.id,
        })

        await this.managementClient.entry.delete({
          environmentId: this.options.environment,
          entryId: option.sys.id,
        })
      }
    } catch (error) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        `Failed to delete product from Contentful: ${error.message}`
      )
    }
  }
}
```

You define a public method `deleteProduct` that accepts a product ID as a parameter.

In the method, you retrieve the product entry from Contentful with its variants, options, and values. For each entry, you must unpublish and delete it.

You now have all the methods necessary to build the creation flow.

### Create Contentful Product Workflow

To implement the logic that's triggered when a product is created in Medusa, or when the admin user triggers a sync manually, you need to create a workflow.

A [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) is a series of actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features.

Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

In this section, you'll create a workflow that creates Medusa products in Contentful using the Contentful Module.

The workflow has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve products to create in Contentful.
- [createProductsContentfulStep](#createProductsContentfulStep): Create the products in Contentful.

Medusa provides the `useQueryGraphStep` in its `@medusajs/medusa/core-flows` package. So, you only need to implement the second step.

#### createProductsContentfulStep

In the second step, you create the retrieved products in Contentful.

To create the step, create the file `src/workflows/steps/create-products-contentful.ts` with the following content:

If you get a type error on resolving the Contentful Module, run the Medusa application once with the `npm run dev` or `yarn dev` command to generate the necessary type definitions, as explained in the [Automatically Generated Types guide](https://docs.medusajs.com/docs/learn/fundamentals/generated-types/index.html.md).

```ts title="src/workflows/steps/create-products-contentful.ts" highlights={createProductsContentfulStepHighlights}
import { ProductDTO } from "@medusajs/framework/types"
import { CONTENTFUL_MODULE } from "../../modules/contentful"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import ContentfulModuleService from "../../modules/contentful/service"
import { EntryProps } from "contentful-management"

type StepInput = {
  products: ProductDTO[]
}

export const createProductsContentfulStep = createStep(
  "create-products-contentful-step",
  async (input: StepInput, { container }) => {
    const contentfulModuleService: ContentfulModuleService = 
      container.resolve(CONTENTFUL_MODULE)

    const products: EntryProps[] = []

    try {
      for (const product of input.products) {
        products.push(await contentfulModuleService.createProduct(product))
      }
    } catch(e) {
      return StepResponse.permanentFailure(
        `Error creating products in Contentful: ${e.message}`,
        products
      )
    }

    return new StepResponse(
      products,
      products
    )
  },
  async (products, { container }) => {
    if (!products) {
      return
    }

    const contentfulModuleService: ContentfulModuleService = 
      container.resolve(CONTENTFUL_MODULE)

    for (const product of products) {
      await contentfulModuleService.deleteProduct(product.sys.id)
    }
  }
)
```

You create a step with `createStep` from the Workflows SDK. It accepts three parameters:

1. The step's unique name, which is `create-products-contentful-step`.
2. An async function that receives two parameters:
   - The step's input, which is in this case an object holding an array of products to create in Contentful.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.
3. An optional compensation function that undoes the actions performed in the step if an error occurs in the workflow's execution. This mechanism ensures data consistency in your application, especially as you integrate external systems.

The Medusa container is different from the module's container. Since modules are isolated, they each have a container with their resources. Refer to the [Module Container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) documentation for more information.

In the step function, you resolve the Contentful Module's service from the Medusa container using the name you exported in the module definition's file.

Then, you iterate over the products and create a new entry in Contentful for each product using the `createProduct` method you created earlier. If the creation of any product fails, you fail the step and pass the created products to the compensation function.

A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters:

1. The step's output, which is the product entries created in Contentful.
2. Data to pass to the step's compensation function.

The compensation function accepts as a parameter the data passed from the step, and an object containing the Medusa container.

In the compensation function, you iterate over the created product entries and delete them from Contentful using the `deleteProduct` method you created earlier.

#### Create the Workflow

Now that you have all the necessary steps, you can create the workflow.

To create the workflow, create the file `src/workflows/create-products-contentful.ts` with the following content:

```ts title="src/workflows/create-products-contentful.ts"
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { createProductsContentfulStep } from "./steps/create-products-contentful"
import { ProductDTO } from "@medusajs/framework/types"

type WorkflowInput = {
  product_ids: string[]
}

export const createProductsContentfulWorkflow = createWorkflow(
  { name: "create-products-contentful-workflow" },
  (input: WorkflowInput) => {
    const { data } = useQueryGraphStep({
      entity: "product",
      fields: [
        "id",
        "title",
        "description",
        "subtitle",
        "status",
        "handle",
        "variants.*",
        "variants.options.*",
        "options.*",
        "options.values.*",
      ],
      filters: {
        id: input.product_ids,
      },
    })
    
    const contentfulProducts = createProductsContentfulStep({
      products: data as unknown as ProductDTO[],
    })

    return new WorkflowResponse(contentfulProducts)
  }
)
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case the product IDs to create in Contentful.

In the workflow's constructor function, you:

1. Retrieve the Medusa products using the `useQueryGraphStep` helper step. This step uses Medusa's [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) tool to retrieve data across modules. You pass it the product IDs to retrieve.
2. Create the product entries in Contentful using the `createProductsContentfulStep` step.

A workflow must return an instance of `WorkflowResponse`. The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is an object of the product entries created in Contentful.

You now have the workflow that you can execute when a product is created in Medusa, or when the admin user triggers a sync manually.

***

## Step 4: Trigger Sync on Product Creation

Medusa has an event system that allows you to listen for events, such as `product.created`, and perform an asynchronous action when the event is emitted.

You listen to events in a subscriber. A [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) is an asynchronous function that listens to one or more events and performs actions when these events are emitted. A subscriber is useful when syncing data across systems, as the operation can be time-consuming and should be performed in the background.

In this step, you'll create a subscriber that listens to the `product.created` event and executes the `createProductsContentfulWorkflow` workflow.

Learn more about subscribers in the [Events and Subscribers documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).

To create a subscriber, create the file `src/subscribers/create-product.ts` with the following content:

```ts title="src/subscribers/create-product.ts" highlights={createProductSubscriberHighlights}
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { 
  createProductsContentfulWorkflow,
} from "../workflows/create-products-contentful"

export default async function handleProductCreate({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await createProductsContentfulWorkflow(container)
    .run({
      input: {
        product_ids: [data.id],
      },
    })

  console.log("Product created in Contentful")
}

export const config: SubscriberConfig = {
  event: "product.created",
}
```

A subscriber file must export:

1. An asynchronous function, which is the subscriber that is executed when the event is emitted.
2. A configuration object that holds the name of the event the subscriber listens to, which is `product.created` in this case.

The subscriber function receives an object as a parameter that has the following properties:

- `event`: An object that holds the event's data payload. The payload of the `product.created` event is an array of product IDs.
- `container`: The Medusa container to access the Framework and commerce tools.

In the subscriber function, you execute the `createProductsContentfulWorkflow` by invoking it, passing the Medusa container as a parameter. Then, you chain a `run` method, passing it the product ID from the event's data payload as input.

Finally, you log a message to the console to indicate that the product was created in Contentful.

### Test the Subscriber

To test out the subscriber, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard and login.

Can't remember the credentials? Learn how to create a user in the [Medusa CLI reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-cli/commands/user/index.html.md).

Next, open the Products page and create a new product.

You should see the following message in the terminal:

```bash
info: Product created in Contentful
```

You can also see the product in the Contentful dashboard by going to the Content page.

***

## Step 5: Trigger Product Sync Manually

The other way to sync products is when the admin user triggers a sync manually. This is useful when you already have products in Medusa and you want to sync them to Contentful.

To allow admin users to trigger a sync manually, you need:

1. A subscriber that listens to a custom event.
2. An API route that emits the custom event when a request is sent to it.
3. A UI route in the Medusa Admin that displays a button to trigger the sync.

### Create Manual Sync Subscriber

You'll start by creating the subscriber that listens to a custom event to sync the Medusa products to Contentful.

To create the subscriber, create the file `src/subscribers/sync-products.ts` with the following content:

```ts title="src/subscribers/sync-products.ts" highlights={syncProductsSubscriberHighlights}
import type { 
  SubscriberConfig,
  SubscriberArgs,
} from "@medusajs/framework"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { 
  createProductsContentfulWorkflow,
} from "../workflows/create-products-contentful"

export default async function syncProductsHandler({
  container,
}: SubscriberArgs<Record<string, unknown>>) {
  const query = container.resolve(ContainerRegistrationKeys.QUERY)
  
  const batchSize = 100
  let hasMore = true
  let offset = 0
  let totalCount = 0

  while (hasMore) {
    const {
      data: products,
      metadata: { count } = {},
    } = await query.graph({
      entity: "product",
      fields: [
        "id",
      ],
      pagination: {
        skip: offset,
        take: batchSize,
      },
    })

    if (products.length) {
      await createProductsContentfulWorkflow(container).run({
        input: {
          product_ids: products.map((product) => product.id),
        },
      })
    }

    hasMore = products.length === batchSize
    offset += batchSize
    totalCount = count ?? 0
  }

  console.log(`Synced ${totalCount} products to Contentful`)
}

export const config: SubscriberConfig = {
  event: "products.sync",
}
```

You create a subscriber that listens to the `products.sync` event.

In the subscriber function, you use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve all the products in Medusa with pagination. Then, for each batch of products, you execute the `createProductsContentfulWorkflow` workflow, passing the product IDs to the workflow.

Finally, you log a message to the console to indicate that the products were synced to Contentful.

### Create API Route to Trigger Sync

Next, to allow the admin user to trigger the sync manually, you need to create an API route that emits the `products.sync` event.

An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts.

Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

So, to create an API route at the path `/admin/contentful/sync`, create the file `src/api/admin/contentful/sync/route.ts` with the following content:

```ts title="src/api/admin/contentful/sync/route.ts" highlights={syncProductsRouteHighlights}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const POST = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const eventService = req.scope.resolve("event_bus")

  await eventService.emit({
    name: "products.sync",
    data: {},
  })

  res.status(200).json({
    message: "Products sync triggered successfully",
  })
}
```

Since you export a `POST` route handler function, you expose an `API` route at `/admin/contentful/sync`. The route handler function accepts two parameters:

1. A request object with details and context on the request, such as body parameters or authenticated user details.
2. A response object to manipulate and send the response.

In the route handler, you resolve the [Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/index.html.md)'s service from the Medusa container and emit the `products.sync` event.

### Create UI Route to Trigger Sync

Finally, you'll add a new page to the Medusa Admin dashboard that displays a button to trigger the sync. To add a page, you need to create a UI route.

A [UI route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) is a React component that specifies the content to be shown in a new page of the Medusa Admin dashboard. You'll create a UI route to display a button that triggers product syncing to Contentful when clicked.

Refer to the [UI Routes](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) documentation for more information.

#### Configure JS SDK

Before creating the UI route, you'll configure Medusa's [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) so that you can use it to send requests to the Medusa server.

The JS SDK is installed by default in your Medusa application. To configure it, create the file `src/admin/lib/sdk.ts` with the following content:

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: "http://localhost:9000",
  debug: process.env.NODE_ENV === "development",
  auth: {
    type: "session",
  },
})
```

You create an instance of the JS SDK using the `Medusa` class from the JS SDK. You pass it an object having the following properties:

- `baseUrl`: The base URL of the Medusa server.
- `debug`: A boolean indicating whether to log debug information into the console.
- `auth`: An object specifying the authentication type. When using the JS SDK for admin customizations, you use the `session` authentication type.

#### Create UI Route

UI routes are created in a `page.tsx` file under a sub-directory of `src/admin/routes` directory. The file's path relative to `src/admin/routes` determines its path in the dashboard.

So, create the file `src/admin/routes/contentful/page.tsx` with the following content:

```tsx title="src/admin/routes/contentful/page.tsx" highlights={contentfulPageHighlights}
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading, Button } from "@medusajs/ui"
import { useMutation } from "@tanstack/react-query"
import { sdk } from "../../lib/sdk"
import { toast } from "@medusajs/ui"

const ContentfulSettingsPage = () => {
  const { mutate, isPending } = useMutation({
    mutationFn: () => 
      sdk.client.fetch("/admin/contentful/sync", {
        method: "POST",
      }),
    onSuccess: () => {
      toast.success("Sync to Contentful triggered successfully")
    },
  })

  return (
    <Container className="p-6">
      <div className="flex flex-col gap-y-4">
        <div>
          <Heading level="h1">Contentful Settings</Heading>
        </div>
        <div>
          <Button 
            variant="primary"
            onClick={() => mutate()}
            isLoading={isPending}
          >
            Sync to Contentful
          </Button>
        </div>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Contentful",
})

export default ContentfulSettingsPage
```

A UI route's file must export:

1. A React component that defines the content of the page.
2. A configuration object that specifies the route's label in the dashboard. This label is used to show a sidebar item for the new route.

In the React component, you use `useMutation` hook from `@tanstack/react-query` to create a mutation that sends a `POST` request to the API route you created earlier. In the mutation function, you use the JS SDK to send the request.

Then, in the return statement, you display a button that triggers the mutation when clicked, which sends a request to the API route you created earlier.

### Test the Sync

To test out the sync, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard and login. In the sidebar, you'll find a new "Contentful" item. If you click on it, you'll see the page you created with the button to trigger the sync.

![The Contentful page in the Medusa Admin dashboard with a button to trigger the sync](https://res.cloudinary.com/dza7lstvk/image/upload/v1744718825/Medusa%20Resources/Screenshot_2025-04-15_at_3.06.53_PM_va8e20.png)

If you click on the button, you'll see the following message in the terminal:

```bash
info: Synced 4 products to Contentful
```

Assuming you have `4` products in Medusa, the message indicates that the sync was successful.

You can also see the products in the Contentful dashboard.

![The Contentful dashboard showing the synced products](https://res.cloudinary.com/dza7lstvk/image/upload/v1744718896/Medusa%20Resources/Screenshot_2025-04-15_at_3.08.02_PM_qexr0x.png)

***

## Step 6: Retrieve Locales API Route

In the next steps, you'll implement customizations that are useful for storefronts. A storefront should show the customer a list of available locales and allow them to select from them.

In this step, you will:

1. Add the logic to retrieve locales from Contentful in the Contentful Module's service.
2. Create an API route that exposes the locales to the storefront.
3. Customize the Next.js Starter Storefront to show the locales to customers.

### Retrieve Locales from Contentful Method

You'll start by adding two methods to the Contentful Module's service that are useful to retrieve locales from Contentful.

The first method retrieves all locales from Contentful. Add it to the service at `src/modules/contentful/service.ts`:

```ts title="src/modules/contentful/service.ts"
export default class ContentfulModuleService {
  // ...
  async getLocales() {
    return await this.managementClient.locale.getMany({})
  }
}
```

You use the `locale.getMany` method of the Contentful Management API client to retrieve all locales.

The second method returns the code of the default locale:

```ts title="src/modules/contentful/service.ts"
export default class ContentfulModuleService {
  // ...
  async getDefaultLocaleCode() {
    return this.options.default_locale
  }
}
```

You return the default locale using the `default_locale` option you set in the module's options.

### Create API Route to Retrieve Locales

Next, you'll create an API route that exposes the locales to the storefront.

To create the API route, create the file `src/api/store/locales/route.ts` with the following content:

```ts title="src/api/store/locales/route.ts" highlights={getLocalesRouteHighlights}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { CONTENTFUL_MODULE } from "../../../modules/contentful"
import ContentfulModuleService from "../../../modules/contentful/service"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const contentfulModuleService: ContentfulModuleService = req.scope.resolve(
    CONTENTFUL_MODULE
  )

  const locales = await contentfulModuleService.getLocales()
  const defaultLocaleCode = await contentfulModuleService.getDefaultLocaleCode()

  const formattedLocales = locales.items.map((locale) => {
    return {
      name: locale.name,
      code: locale.code,
      is_default: locale.code === defaultLocaleCode,
    }
  })

  res.json({
    locales: formattedLocales,
  })
}
```

Since you export a `GET` route handler function, you expose a `GET` route at `/store/locales`.

In the route handler, you resolve the Contentful Module's service from the Medusa container to retrieve the locales and the default locale code.

Then, you format the locales to include their name, code, and whether they are the default locale.

Finally, you return the formatted locales in the JSON response.

### Customize Storefront to Show Locales

In the first step of this tutorial, you installed the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) along with the Medusa application. This storefront provides ecommerce features like a product catalog, a cart, and a checkout.

In this section, you'll customize the storefront to show the locales to customers and allow them to select from them. The selected locale will be stored in the browser's cookies, allowing you to use it later when retrieving a product's localized data.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-contentful`, you can find the storefront by going back to the parent directory and changing to the `medusa-contentful-storefront` directory:

```bash
cd ../medusa-contentful-storefront # change based on your project name
```

#### Add Cookie Functions

You'll start by adding two functions that retrieve and set the locale in the browser's cookies.

In `src/lib/data/cookies.ts` add the following functions:

```ts title="src/lib/data/cookies.ts" highlights={getLocaleHighlights} badgeLabel="Storefront" badgeColor="blue"
export const getLocale = async () => {
  const cookies = await nextCookies()
  return cookies.get("_medusa_locale")?.value
}

export const setLocale = async (locale: string) => {
  const cookies = await nextCookies()
  cookies.set("_medusa_locale", locale, {
    maxAge: 60 * 60 * 24 * 7,
  })
}
```

The `getLocale` function retrieves the locale from the browser's cookies, and the `setLocale` function sets the locale in the browser's cookies.

#### Manage Locales Functions

Next, you'll add server actions to retrieve the locales and set the selected locale.

Create the file `src/lib/data/locale.ts` with the following content:

```ts title="src/lib/data/locale.ts" highlights={getLocalesHighlights} badgeLabel="Storefront" badgeColor="blue"
"use server"

import { sdk } from "@lib/config"
import type { Document } from "@contentful/rich-text-types"
import { getLocale, setLocale } from "./cookies"

export type Locale = {
  name: string
  code: string
  is_default: boolean
}

export async function getLocales() {
  return await sdk.client.fetch<{
    locales: Locale[]
  }>("/store/locales")
}

export async function getSelectedLocale() {
  let localeCode = await getLocale()
  if (!localeCode) {
    const locales = await getLocales()
    localeCode = locales.locales.find((l) => l.is_default)?.code
  }
  return localeCode
}

export async function setSelectedLocale(locale: string) {
  await setLocale(locale)
}
```

You add the following functions:

1. `getLocales`: Retrieves the locales from the Medusa server using the API route you created earlier.
2. `getSelectedLocale`: Retrieves the selected locale from the browser's cookies, or the default locale if no locale is selected.
3. `setSelectedLocale`: Sets the selected locale in the browser's cookies.

You'll use these functions as you add the UI to show the locales next.

#### Show Locales in the Storefront

You'll now add the UI to show the locales to customers and allow them to select from them.

Create the file `src/modules/layout/components/locale-select/index.tsx` with the following content:

```tsx title="src/modules/layout/components/locale-select/index.tsx" highlights={localeSelectHighlights} badgeLabel="Storefront" badgeColor="blue"
"use client"

import { useState, useEffect, Fragment } from "react"
import { getLocales, Locale, getSelectedLocale, setSelectedLocale } from "../../../../lib/data/locale"
import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from "@headlessui/react"
import { ArrowRightMini } from "@medusajs/icons"
import { clx } from "@medusajs/ui"

const LocaleSelect = () => {
  const [locales, setLocales] = useState<Locale[]>([])
  const [locale, setLocale] = useState<Locale | undefined>()
  const [open, setOpen] = useState(false)

  useEffect(() => {
    getLocales()
      .then(({ locales }) => {
        setLocales(locales)
      })
  }, [])

  useEffect(() => {
    if (!locales.length || locale) {
      return
    }

    getSelectedLocale().then((locale) => {
      const localeDetails = locales.find((l) => l.code === locale) 
      setLocale(localeDetails)
    })
  }, [locales])

  useEffect(() => {
    if (locale) {
      setSelectedLocale(locale.code)
    }
  }, [locale])

  const handleChange = (locale: Locale) => {
    setLocale(locale)
    setOpen(false)
  }

  // TODO add return statement
}

export default LocaleSelect
```

You create a `LocaleSelect` component with the following state variables:

1. `locales`: The list of locales retrieved from the Medusa server.
2. `locale`: The selected locale.
3. `open`: A boolean indicating whether the dropdown is open.

Then, you use three `useEffect` hooks:

1. The first `useEffect` hook retrieves the locales using the `getLocales` function and sets them in the `locales` state variable.
2. The second `useEffect` hook is triggered when the `locales` state variable changes. It retrieves the selected locale using the `getSelectedLocale` function and sets the `locale` state variable.
3. The third `useEffect` hook is triggered when the `locale` state variable changes. It sets the selected locale in the browser's cookies using the `setSelectedLocale` function.

You also create a `handleChange` function that sets the selected locale and closes the dropdown. You'll execute this function when the customer selects a locale from the dropdown.

Finally, you'll add a return statement that shows the locale dropdown. Replace the `TODO` with the following:

```tsx title="src/modules/layout/components/locale-select/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div
    className="flex justify-between"
    onMouseEnter={() => setOpen(true)}
    onMouseLeave={() => setOpen(false)}
  >
    <div>
      <Listbox as="span" onChange={handleChange} defaultValue={locale}>
        <ListboxButton className="py-1 w-full">
          <div className="txt-compact-small flex items-start gap-x-2">
            <span>Language:</span>
            {locale && (
              <span className="txt-compact-small flex items-center gap-x-2">
                {locale.name}
              </span>
            )}
          </div>
        </ListboxButton>
        <div className="flex relative w-full min-w-[320px]">
          <Transition
            show={open}
            as={Fragment}
            leave="transition ease-in duration-150"
            leaveFrom="opacity-100"
            leaveTo="opacity-0"
          >
            <ListboxOptions
              className="absolute -bottom-[calc(100%-36px)] left-0 xsmall:left-auto xsmall:right-0 max-h-[442px] overflow-y-scroll z-[900] bg-white drop-shadow-md text-small-regular uppercase text-black no-scrollbar rounded-rounded w-full"
              static
            >
              {locales?.map((l, index) => {
                return (
                  <ListboxOption
                    key={index}
                    value={l}
                    className="py-2 hover:bg-gray-200 px-3 cursor-pointer flex items-center gap-x-2"
                  >
                    {l.name}
                  </ListboxOption>
                )
              })}
            </ListboxOptions>
          </Transition>
        </div>
      </Listbox>
    </div>
    <ArrowRightMini
      className={clx(
        "transition-transform duration-150",
        open ? "-rotate-90" : ""
      )}
    />
  </div>
)
```

You show the selected locale. Then, when the customer hovers over the locale, the dropdown is shown to select a different locale.

When the customer selects a locale, you execute the `handleChange` function, which sets the selected locale and closes the dropdown.

#### Add Locale Select to the Side Menu

The last step is to show the locale selector in the side menu after the country selector.

In `src/modules/layout/components/side-menu/index.tsx`, add the following import:

```tsx title="src/modules/layout/components/side-menu/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import LocaleSelect from "../locale-select"
```

Then, add the `LocaleSelect` component in the return statement of the `SideMenu` component, after the `div` wrapping the country selector:

```tsx title="src/modules/layout/components/side-menu/index.tsx" badgeLabel="Storefront" badgeColor="blue"
<LocaleSelect />
```

The locale selector will now show in the side menu after the country selector.

### Test out the Locale Selector

To test out all the changes made in this step, start the Medusa application by running the following command in the Medusa application's directory:

```bash npm2yarn
npm run dev
```

Then, start the Next.js Starter Storefront by running the following command in the storefront's directory:

```bash npm2yarn
npm run dev
```

The storefront will run at `http://localhost:8000`. Open it in your browser, then click on "Menu" at the top right. You'll see at the bottom of the side menu the locale selector.

![The locale selector in the side menu](https://res.cloudinary.com/dza7lstvk/image/upload/v1744720332/Medusa%20Resources/Screenshot_2025-04-15_at_3.31.51_PM_vdqou5.png)

You can try selecting a different locale. The selected locale will be stored, but products will still be shown in the default locale. You'll implement the locale-based product retrieval in the next step.

***

## Step 7: Retrieve Product Details for Locale

The next feature you'll implement is retrieving and displaying product details for a selected locale.

You'll implement this feature by:

1. Linking Medusa's product to Contentful's product.
2. Adding the method to retrieve product details for a selected locale from Contentful.
3. Adding a new route to retrieve the product details for a selected locale.
4. Customizing the storefront to show the product details for the selected locale.

### Link Medusa's Product to Contentful's Product

Medusa facilitates retrieving data across systems using [module links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md). A module link forms an association between data models of two modules while maintaining module isolation.

Not only do module links support Medusa data models, but they also support virtual data models that are not persisted in Medusa's database. In that case, you create a [read-only module link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/read-only/index.html.md) that allows you to retrieve data across systems.

In this section, you'll define a read-only module link between Medusa's product and Contentful's product, allowing you to later retrieve a product's entry in Contentful within a single query.

Learn more about read-only module links in the [Read-Only Module Links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/read-only/index.html.md) documentation.

Module links are defined in a TypeScript or JavaScript file under the `src/links` directory. So, create the file `src/links/product-contentful.ts` with the following content:

```ts title="src/links/product-contentful.ts" highlights={productContentfulLinkHighlights}
import { defineLink } from "@medusajs/framework/utils"
import ProductModule from "@medusajs/medusa/product"
import { CONTENTFUL_MODULE } from "../modules/contentful"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    field: "id",
  },
  {
    linkable: {
      serviceName: CONTENTFUL_MODULE,
      alias: "contentful_product",
      primaryKey: "product_id",
    },
  },
  {
    readOnly: true,
  }
)
```

You define a module link using `defineLink` from the Modules SDK. It accepts three parameters:

1. An object with the linkable configuration of the data model in Medusa, and the field that will be passed as a filter to the Contentful Module's service.
2. An object with the linkable configuration of the virtual data model in Contentful. This object must have the following properties:
   - `serviceName`: The name of the service, which is the Contentful Module's name. Medusa uses this name to resolve the module's service from the Medusa container.
   - `alias`: The alias to use when querying the linked records. You'll see how that works in a bit.
   - `primaryKey`: The field in Contentful's virtual data model that holds the ID of a product.
3. An object with the `readOnly` property set to `true`.

You'll see how the module link works in the upcoming steps.

### List Contentful Products Method

Next, you'll add a method that lists Contentful products for a given locale.

Add the following method to the Contentful Module's service at `src/modules/contentful/service.ts`:

```ts title="src/modules/contentful/service.ts" highlights={listContentfulProductsMethodHighlights}
export default class ContentfulModuleService {
  // ...
  async list(
    filter: {
      id: string | string[]
      context?: {
        locale: string
      }
    }
  ) {
    const contentfulProducts = await this.deliveryClient.getEntries({
      limit: 15,
      content_type: "product",
      "fields.medusaId": filter.id,
      locale: filter.context?.locale,
      include: 3,
    })

    return contentfulProducts.items.map((product) => {
      // remove links
      const { productVariants: _, productOptions: __, ...productFields } = product.fields
      return {
        ...productFields,
        product_id: product.fields.medusaId,
        variants: product.fields.productVariants.map((variant) => {
          // remove circular reference
          const { product: _, productOptionValues: __, ...variantFields } = variant.fields
          return {
            ...variantFields,
            product_variant_id: variant.fields.medusaId,
            options: variant.fields.productOptionValues.map((option) => {
              // remove circular reference
              const { productOption: _, ...optionFields } = option.fields
              return {
                ...optionFields,
                product_option_id: option.fields.medusaId,
              }
            }),
          }
        }),
        options: product.fields.productOptions.map((option) => {
          // remove circular reference
          const { product: _, ...optionFields } = option.fields
          return {
            ...optionFields,
            product_option_id: option.fields.medusaId,
            values: option.fields.values.map((value) => {
              // remove circular reference
              const { productOptionValue: _, ...valueFields } = value.fields
              return {
                ...valueFields,
                product_option_value_id: value.fields.medusaId,
              }
            }),
          }
        }),
      }
    })
  }
}
```

You add a `list` method that accepts an object with the following properties:

1. `id`: The ID of the product(s) in Medusa to retrieve their entries in Contentful.
2. `context`: An object with the `locale` property that holds the locale code to retrieve the product's entry in Contentful for that locale.

In the method, you use the Delivery API client's `getEntries` method to retrieve the products. You pass the following parameters:

- `limit`: The maximum number of products to retrieve.
- `content_type`: The content type of the entries to retrieve, which is `product`.
- `fields.medusaId`: Filter the products by their `medusaId` field, which holds the ID of the product in Medusa.
- `locale`: The locale code to retrieve the fields of the product in that locale.
- `include`: The depth of the included nested entries. This ensures that you can retrieve the product's variants and options, and their values.

Then, you format the retrieved products to:

- Pass the product's ID in the `product_id` property. This is essential to map a product in Medusa to its entry in Contentful.
- Remove the circular references in the product's variants, options, and values to avoid infinite loops.

To paginate the retrieved products, implement a `listAndCount` method as explained in the [Query Context](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query-context#using-pagination-with-query/index.html.md) documentation.

### Retrieve Product Details for Locale API Route

You'll now create the API route that returns a product's details for a given locale.

You can create an API route that accepts path parameters by creating a directory within the route file's path whose name is of the format `[param]`.

So, create the file `src/api/store/products/[id]/[locale]/route.ts` with the following content:

```ts title="src/api/store/products/[id]/[locale]/route.ts" highlights={getProductLocaleDetailsRouteHighlights}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { QueryContext } from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const { locale, id } = req.params
  
  const query = req.scope.resolve("query")

  const { data } = await query.graph({
    entity: "product",
    fields: [
      "id",
      "contentful_product.*",
    ],
    filters: {
      id,
    },
    context: {
      contentful_product: QueryContext({
        locale,
      }),
    },
  })

  res.json({
    product: data[0],
  })  
}
```

Since you export a `GET` route handler function, you expose a `GET` route at `/store/products/[id]/[locale]`. The route accepts two path parameters: the product's ID and the locale code.

In the route handler, you retrieve the `locale` and `id` path parameters from the request. Then, you resolve [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) from the Medusa container.

Next, you use Query to retrieve the localized details of the specified product. To do that, you pass an object with the following properties:

- `entity`: The entity to retrieve, which is `product`.
- `fields`: The fields to retrieve. Notice that you include the `contentful_product.*` field, which is available through the module link you created earlier.
- `filters`: The filter to apply on the retrieved products. You apply the product's ID as a filter.
- `context`: An additional context to be passed to the methods retrieving the data. To pass a context, you use [Query Context](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query-context/index.html.md).

By specifying `contentful_product.*` in the `fields` property, Medusa will retrieve the product's entry from Contentful using the `list` method you added to the Contentful Module's service.

Medusa passes the filters and context to the `list` method, and attaches the returned data to the Medusa product if its `product_id` matches the product's ID.

Finally, you return the product's details in the JSON response.

You can now use this route to retrieve a product's details for a given locale.

### Show Localized Product Details in Storefront

Now that you expose the localized product details, you can customize the storefront to show them.

#### Install Contentful Rich Text Package

When you retrieve the entries from Contentful, rich-text fields are returned as an object that requires special rendering. So, Contentful provides a package to render rich-text fields.

Install the package by running the following command:

```bash npm2yarn
npm install @contentful/@contentful/rich-text-types
```

You'll use this package to render the product's description.

#### Retrieve Localized Product Details Function

To retrieve a product's details for a given locale, you'll add a function that sends a request to the API route you created.

First, add the following import at the top of `src/lib/data/locale.ts`:

```ts title="src/lib/data/locale.ts" badgeLabel="Storefront" badgeColor="blue"
import type { Document } from "@contentful/rich-text-types"
```

Then, add the following type and function at the end of the file:

```ts title="src/lib/data/locale.ts" badgeLabel="Storefront" badgeColor="blue"
export type ProductLocaleDetails = {
  id: string
  contentful_product: {
    product_id: string
    title: string
    handle: string
    description: Document
    subtitle?: string
    variants: {
      title: string
      product_variant_id: string
      options: {
        value: string
        product_option_id: string
      }[]
    }[]
    options: {
      title: string
      product_option_id: string
      values: {
        title: string
        product_option_value_id: string
      }[]
    }[]
  }
}

export async function getProductLocaleDetails(
  productId: string
) {
  const localeCode = await getSelectedLocale()

  return await sdk.client.fetch<{
    product: ProductLocaleDetails
  }>(`/store/products/${productId}/${localeCode}`)
}
```

You define a `ProductLocaleDetails` type that describes the structure of a localized product's details.

You also define a `getProductLocaleDetails` function that sends a request to the API route you created and returns the localized product's details.

#### Show Localized Product Title in Products Listing

Next, you'll customize existing components to show the localized product details.

The component defined in `src/modules/products/components/product-preview/index.tsx` shows the product's details in the products listing page. You need to retrieve the localized product details and show the product's title in the selected locale.

In `src/modules/products/components/product-preview/index.tsx`, add the following import:

```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { getProductLocaleDetails } from "@lib/data/locale"
```

Then, in the `ProductPreview` component in the same file, add the following before the `return` statement:

```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const productLocaleDetails = await getProductLocaleDetails(product.id!)
```

This will retrieve the localized product details for the selected locale.

Finally, to show the localized product title, find in the `ProductPreview` component's `return` statement the following line:

```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
{product.title}
```

And replace it with the following:

```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
{productLocaleDetails.product.contentful_product?.title || product.title}
```

You'll test it out after the next step.

#### Show Localized Product Details in Product Page

Next, you'll customize the product page to show the localized product details.

The product's details page is defined in `src/app/[countryCode]/(main)/products/[handle]/page.tsx`. So, add the following import at the top of the file:

```tsx title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue"
import { getProductLocaleDetails } from "@lib/data/locale"
```

Then, in the `ProductPage` component in the same file, add the following before the `return` statement:

```tsx title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue"
const productLocaleDetails = await getProductLocaleDetails(pricedProduct.id!)
```

This will retrieve the localized product details for the selected locale.

Finally, in the `ProductPage` component in the same file, pass the following prop to `ProductTemplate`:

```tsx title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <ProductTemplate
    // ...
    productLocaleDetails={productLocaleDetails.product}
  />
)
```

Next, you'll customize the `ProductTemplate` component to accept and use this prop.

In `src/modules/products/templates/index.tsx`, add the following import:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { ProductLocaleDetails } from "@lib/data/locale"
```

Then, update the `ProductTemplateProps` type to include the `productLocaleDetails` prop:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
export type ProductTemplateProps = {
  // ...
  productLocaleDetails: ProductLocaleDetails
}
```

Next, update the `ProductTemplate` component to destructure the `productLocaleDetails` prop:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const ProductTemplate: React.FC<ProductTemplateProps> = ({
  // ...
  productLocaleDetails,
}) => {
  // ...
}
```

Finally, pass the `productLocaleDetails` prop to the `ProductInfo` component in the `return` statement:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
<ProductInfo
  // ...
  productLocaleDetails={productLocaleDetails}
/>
```

The `ProductInfo` component shows the product's details. So, you need to update it to accept and use the `productLocaleDetails` prop.

In `src/modules/products/templates/product-info/index.tsx`, add the following imports:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { ProductLocaleDetails } from "@lib/data/locale"
import { documentToHtmlString } from "@contentful/rich-text-html-renderer"
```

Then, update the `ProductInfoProps` type to include the `productLocaleDetails` prop:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
export type ProductInfoProps = {
  // ...
  productLocaleDetails: ProductLocaleDetails
}
```

Next, update the `ProductInfo` component to destructure the `productLocaleDetails` prop:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const ProductInfo = ({ product, productLocaleDetails }: ProductInfoProps) => {
  // ...
}
```

Then, find the following line in the `return` statement:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
{product.title}
```

And replace it with the following:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
{productLocaleDetails.contentful_product?.title || product.title}
```

Also, find the following line:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
{product.description}
```

And replace it with the following:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
{productLocaleDetails.contentful_product?.description ? 
  <div dangerouslySetInnerHTML={{ __html: documentToHtmlString(productLocaleDetails.contentful_product?.description) }} /> : 
  product.description
}
```

You use the `documentToHtmlString` function to render the rich-text field. The function returns an HTML string that you can use to render the description.

### Test out the Localized Product Details

You can now test out all the changes made in this step.

To do that, start the Medusa application by running the following command in the Medusa application's directory:

```bash npm2yarn
npm run dev
```

Then, start the Next.js Starter Storefront by running the following command in the storefront's directory:

```bash npm2yarn
npm run dev
```

Open the storefront at `http://localhost:8000` and select a different locale.

Then, open the products listing page by clicking on Menu -> Store. You'll see the product titles in the selected locale.

![The product titles are shown in the selected locale](https://res.cloudinary.com/dza7lstvk/image/upload/v1744723796/Medusa%20Resources/Screenshot_2025-04-15_at_4.29.39_PM_izxt6j.png)

Then, if you click on a product, you'll see the product's title and description in the selected locale.

![The product's title and description are shown in the selected locale](https://res.cloudinary.com/dza7lstvk/image/upload/v1744723844/Medusa%20Resources/Screenshot_2025-04-15_at_4.30.31_PM_pt6ngz.png)

***

## Step 8: Sync Changes from Contentful to Medusa

The last feature you'll implement is syncing changes from Contentful to Medusa.

Contentful's webhooks allow you to listen to changes in your Contentful entries. You can then set up a webhook listener API route in Medusa that updates the product's data.

In this step, you'll set up a webhook listener that updates Medusa's product data when a Contentful entry is published.

### Prerequisites: Public Server

Webhooks can only trigger deployed listeners. So, you must either [deploy your Medusa application](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/deployment/index.html.md), or use tools like [ngrok](https://ngrok.com/) to publicly expose your local application.

### Set Up Webhooks in Contentful

Before setting up the webhook listener, you need to set up a webhook in Contentful. To do that, on the Contentful dashboard:

1. Click on the cog icon at the top right, then choose "Webhooks" from the dropdown.

![The cog icon is at the top right next to the avatar icon. When you click on it, you see a dropdown with the "Webhooks" option](https://res.cloudinary.com/dza7lstvk/image/upload/v1744733023/Medusa%20Resources/Screenshot_2025-04-15_at_7.03.21_PM_ji7ohw.png)

2. On the Webhooks page, click on the "Add Webhook" button.
3. In the webhook form:
   - In the Name fields, enter a name, such as "Medusa".
   - In the URL field, enter `{your_app}/hooks/contentful`, where `{your_app}` is the public URL of your Medusa application. You'll create the `/hooks/contentful` API route in a bit.
   - In the Triggers section, select the "Published" trigger for "Entry".

![The webhook form with the URL and triggers highlighted](https://res.cloudinary.com/dza7lstvk/image/upload/v1744733332/Medusa%20Resources/Screenshot_2025-04-15_at_7.08.24_PM_coq6oo.png)

- Scroll down to the "Headers" section, and choose "application/json" for the "Content type" field.

![The webhook form](https://res.cloudinary.com/dza7lstvk/image/upload/v1744733411/Medusa%20Resources/Screenshot_2025-04-15_at_7.09.53_PM_kvlppv.png)

4. Once you're done, click the Save button.

### Setup Webhook Secret in Contentful

You also need to add a webhook secret in Contentful. To do that, on the Contentful dashboard:

1. Click on the cog icon at the top right, then choose "Webhooks" from the dropdown.
2. On the Webhooks page, click on the "Settings" tab.
3. Click on the "Enable request verification" button.

![The "Enable request verification" button in the "Settings" tab](https://res.cloudinary.com/dza7lstvk/image/upload/v1744786362/Medusa%20Resources/Screenshot_2025-04-16_at_9.51.39_AM_byv3tt.png)

4. Copy the secret that shows up. You can update it later but you can't see the same secret again.

You'll use the secret to verify the webhook request in Medusa next.

### Update Contentful Module Options

First, add the webhook secret as an environment variable in the Medusa application's `.env` file:

```plain
CONTENTFUL_WEBHOOK_SECRET=aEl7...
```

Next, add the webhook secret to the Contentful Module options in the Medusa application's `medusa-config.ts` file:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/contentful",
      options: {
        // ...
        webhook_secret: process.env.CONTENTFUL_WEBHOOK_SECRET,
      },
    },
  ],
})
```

Finally, update the `ModuleOptions` type in `src/modules/contentful/loader/create-content-models.ts` to include the `webhook_secret` option:

```ts title="src/modules/contentful/loader/create-content-models.ts"
export type ModuleOptions = {
  // ...
  webhook_secret: string
}
```

### Add Verify Request Method

Next, you'll add a method to the Contentful Module's service that verifies a webhook request.

To verify the request, you'll need the `@contentful/node-apps-toolkit` package that provides utility functions for Node.js applications.

So, run the following command to install it:

```bash npm2yarn
npm install @contentful/node-apps-toolkit
```

Then, add the following method to the Contentful Module's service in `src/modules/contentful/service.ts`:

```ts title="src/modules/contentful/service.ts"
// other imports...
import { 
  CanonicalRequest, 
  verifyRequest,
} from "@contentful/node-apps-toolkit"

export default class ContentfulModuleService {
  // ...
  async verifyWebhook(request: CanonicalRequest) {
    if (!this.options.webhook_secret) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA, 
        "Webhook secret is not set"
      )
    }
    return verifyRequest(this.options.webhook_secret, request, 0)
  }
}
```

You add a `verifyWebhook` method that verifies a webhook request using the `verifyRequest` function.

You pass to the `verifyRequest` function the webhook secret from the module's options with the request's details. You also disable time-to-live (TTL) check by passing `0` as the third argument.

### Handle Contentful Webhook Workflow

Before you add the webhook listener, the last piece you need is to add a workflow that handles the necessary updates based on the webhook event.

The workflow will have the following steps:

- [prepareUpdateDataStep](#prepareUpdateDataStep): Prepare the data for the update

You only need to implement the first step, as Medusa provides the other workflows (running as steps) in the `@medusajs/medusa/core-flows` package.

#### prepareUpdateDataStep

The first step receives the webhook data payload and, based on the entry type, returns the data necessary for the update.

To create the step, create the file `src/workflows/steps/prepare-update-data.ts` with the following content:

```ts title="src/workflows/steps/prepare-update-data.ts" highlights={prepareUpdateDataStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { EntryProps } from "contentful-management"
import ContentfulModuleService from "../../modules/contentful/service"
import { CONTENTFUL_MODULE } from "../../modules/contentful"

type StepInput = {
  entry: EntryProps
}

export const prepareUpdateDataStep = createStep(
  "prepare-update-data",
  async ({ entry }: StepInput, { container }) => {
    const contentfulModuleService: ContentfulModuleService = 
      container.resolve(CONTENTFUL_MODULE)
    
    const defaultLocale = await contentfulModuleService.getDefaultLocaleCode()

    let data: Record<string, unknown> = {}

    switch (entry.sys.contentType.sys.id) {
      case "product":
        data = {
          id: entry.fields.medusaId[defaultLocale!],
          title: entry.fields.title[defaultLocale!],
          subtitle: entry.fields.subtitle?.[defaultLocale!] || undefined,
          handle: entry.fields.handle[defaultLocale!],
        }
        break
      case "productVariant":
        data = {
          id: entry.fields.medusaId[defaultLocale!],
          title: entry.fields.title[defaultLocale!],
        }
        break
      case "productOption":
        data = {
          selector: {
            id: entry.fields.medusaId[defaultLocale!],
          },
          update: {
            title: entry.fields.title[defaultLocale!],
          },
        }
        break
    }

    return new StepResponse(data)
  }
)
```

You define a `prepareUpdateDataStep` function that receives the webhook data payload as an input.

In the step, you resolve the Contentful Module's service and use it to retrieve the default locale code. You need it to find the value to update the fields in Medusa.

Next, you prepare the data to return based on the entry type:

- `product`: The product's ID, title, subtitle, and handle.
- `productVariant`: The product variant's ID and title.
- `productOption`: The product option's ID and title.

The data is prepared based on the expected input for the workflows that will be used to update the data.

#### Create the Workflow

You can now create the workflow that handles the webhook event.

Create the file `src/workflows/handle-contentful-hook.ts` with the following content:

```ts title="src/workflows/handle-contentful-hook.ts" highlights={handleContentfulHookWorkflowHighlights} collapsibleLines="1-14" expandButtonLabel="Show Imports"
import { createWorkflow, when } from "@medusajs/framework/workflows-sdk"
import { EntryProps } from "contentful-management"
import { prepareUpdateDataStep } from "./steps/prepare-update-data"
import { 
  updateProductOptionsWorkflow, 
  updateProductsWorkflow, 
  updateProductVariantsWorkflow, 
  UpdateProductOptionsWorkflowInput,
} from "@medusajs/medusa/core-flows"
import { 
  UpsertProductDTO, 
  UpsertProductVariantDTO,
} from "@medusajs/framework/types"

export type WorkflowInput = {
  entry: EntryProps
}

export const handleContentfulHookWorkflow = createWorkflow(
  { name: "handle-contentful-hook-workflow" },
  (input: WorkflowInput) => {
    const prepareUpdateData = prepareUpdateDataStep({
      entry: input.entry,
    })

    when(input, (input) => input.entry.sys.contentType.sys.id === "product")
      .then(() => {
        updateProductsWorkflow.runAsStep({
          input: {
            products: [prepareUpdateData as UpsertProductDTO],
          },
        })
      })

    when(input, (input) => 
      input.entry.sys.contentType.sys.id === "productVariant"
    )
    .then(() => {
      updateProductVariantsWorkflow.runAsStep({
        input: {
            product_variants: [prepareUpdateData as UpsertProductVariantDTO],
          },
        })
      })

    when(input, (input) => 
      input.entry.sys.contentType.sys.id === "productOption"
    )
    .then(() => {
      updateProductOptionsWorkflow.runAsStep({
        input: prepareUpdateData as unknown as UpdateProductOptionsWorkflowInput,
      })
    })
  }
)
```

You define a `handleContentfulHookWorkflow` function that receives the webhook data payload as an input.

In the workflow, you:

- Prepare the data for the update using the `prepareUpdateDataStep` step.
- Use a [when](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) condition to check if the entry type is a `product`, and if so, update the product using the `updateProductsWorkflow`.
- Use a `when` condition to check if the entry type is a `productVariant`, and if so, update the product variant using the `updateProductVariantsWorkflow`.
- Use a `when` condition to check if the entry type is a `productOption`, and if so, update the product option using the `updateProductOptionsWorkflow`.

You can't perform data manipulation in a workflow's constructor function. Instead, the Workflows SDK includes utility functions like `when` to perform typical operations that requires accessing data values. Learn more about workflow constraints in the [Workflow Constraints](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documentation.

### Add the Webhook Listener API Route

You can finally add the API route that acts as a webhook listener.

To add the API route, create the file `src/api/hooks/contentful/route.ts` with the following content:

```ts title="src/api/hooks/contentful/route.ts" highlights={contentfulHookRouteHighlights} collapsibleLines="1-10" expandButtonLabel="Show Imports"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { 
  handleContentfulHookWorkflow, 
  HandleContentfulHookWorkflowInput,
} from "../../../workflows/handle-contentful-hook"
import { CONTENTFUL_MODULE } from "../../../modules/contentful"
import { CanonicalRequest } from "@contentful/node-apps-toolkit"
import { MedusaError } from "@medusajs/framework/utils"
import ContentfulModuleService from "../../../modules/contentful/service"

export const POST = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const contentfulModuleService: ContentfulModuleService = 
    req.scope.resolve(CONTENTFUL_MODULE)
  
  const isValid = await contentfulModuleService.verifyWebhook({
    path: req.path,
    method: req.method.toUpperCase(),
    headers: req.headers,
    body: JSON.stringify(req.body),
  } as unknown as CanonicalRequest)
  
  if (!isValid) {
    throw new MedusaError(
      MedusaError.Types.UNAUTHORIZED, 
      "Invalid webhook request"
    )
  }
          
  await handleContentfulHookWorkflow(req.scope).run({
    input: {
      entry: req.body,
    } as unknown as HandleContentfulHookWorkflowInput,
  })

  res.status(200).send("OK")
}
```

Since you export a `POST` route handler function, you expose a `POST` route at `/hooks/contentful`.

In the route, you first use the `verifyWebhook` method of the Contentful Module's service to verify the request. If the request is invalid, you throw an error.

Then, you run the `handleContentfulHookWorkflow` passing the request's body, which is the webhook data payload, as an input.

Finally, you return a `200` response to Contentful to confirm that the webhook was received and processed.

### Test the Webhook Listener

To test out the webhook listener, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, try updating a product's title (in the default locale) in Contentful. You should see the product's title updated in Medusa.

***

## Next Steps

You've now integrated Contentful with Medusa and supported localized product details. You can expand on the features in this tutorial to:

1. Add support for other data types, such as product categories or collections.
   - Refer to the data model references for each [Commerce Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) to figure out the content types you need to create in Contentful.
2. Listen to other product events and update the Contentful entries accordingly.
   - Refer to the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for details on all events emitted in Medusa.
3. Add localization for the entire Next.js Starter Storefront. You can either:
   - Create content types in Contentful for different sections in the storefront, then use them to retrieve the localized content;
   - Or use the approaches recommended in the [Next.js documentation](https://nextjs.org/docs/app/building-your-application/routing/internationalization).

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.
3. Contact the [sales team](https://medusajs.com/contact/) to get help from the Medusa team.


# How to Build Magento Data Migration Plugin

In this tutorial, you'll learn how to build a [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) that migrates data, such as products, from Magento to Medusa.

Magento is known for its customization capabilities. However, its monolithic architecture imposes limitations on business requirements, often forcing development teams to implement hacky workarounds. Over time, these customizations become challenging to maintain, especially as the business scales, leading to increased technical debt and slower feature delivery.

Medusa's modular architecture allows you to build a custom digital commerce platform that meets your business requirements without the limitations of a monolithic system. By migrating from Magento to Medusa, you can take advantage of Medusa's modern technology stack to build a scalable and flexible commerce platform that grows with your business.

By following this tutorial, you'll create a Medusa plugin that migrates data from a Magento server to a Medusa application in minimal time. You can re-use this plugin across multiple Medusa applications, allowing you to adopt Medusa across your projects.

## Summary

### Prerequisites



This tutorial will teach you how to:

- Install and set up a Medusa application project.
- Install and set up a Medusa plugin.
- Implement a Magento Module in the plugin to connect to Magento's APIs and retrieve products.
  - This guide will only focus on migrating product data from Magento to Medusa. You can extend the implementation to migrate other data, such as customers, orders, and more.
- Trigger data migration from Magento to Medusa in a scheduled job.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Diagram showcasing the flow of migrating data from Magento to Medusa](https://res.cloudinary.com/dza7lstvk/image/upload/v1739360550/Medusa%20Resources/magento-summary_hsewci.jpg)

[Example Repository](https://github.com/medusajs/examples/tree/main/migrate-from-magento): Find the full code of the guide in this repository. The repository also includes additional features, such as triggering migrations from the Medusa Admin dashboard.

***

## Step 1: Install a Medusa Application

You'll first install a Medusa application that exposes core commerce features through REST APIs. You'll later install the Magento plugin in this application to test it out.

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll be asked for the project's name. You can also optionally choose to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).

Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Refer to the [Medusa Architecture](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md) documentation to learn more.

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Install a Medusa Plugin Project

A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. You can add in the plugin [API Routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), and other customizations, as you'll see in this guide. Afterward, you can test it out locally in a Medusa application, then publish it to npm to install and use it in any Medusa application.

Refer to the [Plugins](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) documentation to learn more about plugins.

A Medusa plugin is set up in a different project, giving you the flexibility in building and publishing it, while providing you with the tools to test it out locally in a Medusa application.

To create a new Medusa plugin project, run the following command in a directory different than that of the Medusa application:

```bash npm2yarn
npx create-medusa-app@latest medusa-plugin-magento --plugin
```

Where `medusa-plugin-magento` is the name of the plugin's directory and the name set in the plugin's `package.json`. So, if you wish to publish it to NPM later under a different name, you can change it here in the command or later in `package.json`.

Once the installation process is done, a new directory named `medusa-plugin-magento` will be created with the plugin project files.

![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg)

***

## Step 3: Set up Plugin in Medusa Application

Before you start your development, you'll set up the plugin in the Medusa application you installed in the first step. This will allow you to test the plugin during your development process.

In the plugin's directory, run the following command to publish the plugin to the local package registry:

```bash title="Plugin project"
npx medusa plugin:publish
```

This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`.

Next, you'll install the plugin in the Medusa application from the local registry.

If you've installed your Medusa project before v2.3.1, you must install [yalc](https://github.com/wclr/yalc) as a development dependency first.

Run the following command in the Medusa application's directory to install the plugin:

```bash title="Medusa application"
npx medusa plugin:add medusa-plugin-magento
```

This command installs the plugin in the Medusa application from the local package registry.

Next, register the plugin in the `medusa-config.ts` file of the Medusa application:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  plugins: [
    {
      resolve: "medusa-plugin-magento",
      options: {
        // TODO add options
      },
    },
  ],
})
```

You add the plugin to the array of plugins. Later, you'll pass options useful to retrieve data from Magento.

Finally, to ensure your plugin's changes are constantly published to the local registry, simplifying your testing process, keep the following command running in the plugin project during development:

```bash title="Plugin project"
npx medusa plugin:develop
```

***

## Step 4: Implement Magento Module

To connect to external applications in Medusa, you create a custom module. A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

In this step, you'll create a Magento Module in the Magento plugin that connects to a Magento server's REST APIs and retrieves data, such as products.

Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules.

### Create Module Directory

A module is created under the `src/modules` directory of your plugin. So, create the directory `src/modules/magento`.

![Diagram showcasing the module directory to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739272368/magento-1_ikev4x.jpg)

### Create Module's Service

You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to external systems or the database, which is useful if your module defines tables in the database.

In this section, you'll create the Magento Module's service that connects to Magento's REST APIs and retrieves data.

Start by creating the file `src/modules/magento/service.ts` in the plugin with the following content:

![Diagram showcasing the service file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739272483/magento-2_ajetpr.jpg)

```ts title="src/modules/magento/service.ts"
type Options = {
  baseUrl: string
  storeCode?: string
  username: string
  password: string
  migrationOptions?: {
    imageBaseUrl?: string
  }
}

export default class MagentoModuleService {
  private options: Options

  constructor({}, options: Options) {
    this.options = {
      ...options,
      storeCode: options.storeCode || "default",
    }
  }
}
```

You create a `MagentoModuleService` that has an `options` property to store the module's options. These options include:

- `baseUrl`: The base URL of the Magento server.
- `storeCode`: The store code of the Magento store, which is `default` by default.
- `username`: The username of a Magento admin user to authenticate with the Magento server.
- `password`: The password of the Magento admin user.
- `migrationOptions`: Additional options useful for migrating data, such as the base URL to use for product images.

The service's constructor accepts as a first parameter the [Module Container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md), which allows you to access resources available for the module. As a second parameter, it accepts the module's options.

### Add Authentication Logic

To authenticate with the Magento server, you'll add a method to the service that retrieves an access token from Magento using the username and password in the options. This access token is used in subsequent requests to the Magento server.

First, add the following property to the `MagentoModuleService` class:

```ts title="src/modules/magento/service.ts"
export default class MagentoModuleService {
  private accessToken: {
    token: string
    expiresAt: Date
  }
  // ...
}
```

You add an `accessToken` property to store the access token and its expiration date. The access token Magento returns expires after four hours, so you store the expiration date to know when to refresh the token.

Next, add the following `authenticate` method to the `MagentoModuleService` class:

```ts title="src/modules/magento/service.ts"
import { MedusaError } from "@medusajs/framework/utils"

export default class MagentoModuleService {
  // ...
  async authenticate() {
    const response = await fetch(`${this.options.baseUrl}/rest/${this.options.storeCode}/V1/integration/admin/token`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ username: this.options.username, password: this.options.password }),
    })

    const token = await response.text()

    if (!response.ok) {
      throw new MedusaError(MedusaError.Types.UNAUTHORIZED, `Failed to authenticate with Magento: ${token}`)
    }

    this.accessToken = {
      token: token.replaceAll("\"", ""),
      expiresAt: new Date(Date.now() + 4 * 60 * 60 * 1000), // 4 hours in milliseconds
    }
  }
}
```

You create an `authenticate` method that sends a POST request to the Magento server's `/rest/{storeCode}/V1/integration/admin/token` endpoint, passing the username and password in the request body.

If the request is successful, you store the access token and its expiration date in the `accessToken` property. If the request fails, you throw a `MedusaError` with the error message returned by Magento.

Lastly, add an `isAccessTokenExpired` method that checks if the access token has expired:

```ts title="src/modules/magento/service.ts"
export default class MagentoModuleService {
  // ...
  async isAccessTokenExpired(): Promise<boolean> {
    return !this.accessToken || this.accessToken.expiresAt < new Date()
  }
}
```

In the `isAccessTokenExpired` method, you return a boolean indicating whether the access token has expired. You'll use this in later methods to check if you need to refresh the access token.

### Retrieve Products from Magento

Next, you'll add a method that retrieves products from Magento. Due to limitations in Magento's API that makes it difficult to differentiate between simple products that don't belong to a configurable product and those that do, you'll only retrieve configurable products and their children. You'll also retrieve the configurable attributes of the product, such as color and size.

First, you'll add some types to represent a Magento product and its attributes. Create the file `src/modules/magento/types.ts` in the plugin with the following content:

![Diagram showcasing the types file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739346287/Medusa%20Resources/magento-3_fpghog.jpg)

```ts title="src/modules/magento/types.ts"
export type MagentoProduct = {
  id: number
  sku: string
  name: string
  price: number
  status: number
  // not handling other types
  type_id: "simple" | "configurable"
  created_at: string
  updated_at: string
  extension_attributes: {
    category_links: {
      category_id: string
    }[]
    configurable_product_links?: number[] 
    configurable_product_options?: {
      id: number
      attribute_id: string
      label: string
      position: number
      values: {
        value_index: number
      }[]
    }[]
  }
  media_gallery_entries: {
    id: number
    media_type: string
    label: string
    position: number
    disabled: boolean
    types: string[]
    file: string
  }[]
  custom_attributes: {
    attribute_code: string
    value: string
  }[]
  // added by module
  children?: MagentoProduct[]
}

export type MagentoAttribute = {
  attribute_code: string
  attribute_id: number
  default_frontend_label: string
  options: {
    label: string
    value: string
  }[]
}

export type MagentoPagination = {
  search_criteria: {
    filter_groups: [],
    page_size: number
    current_page: number
  }
  total_count: number
}

export type MagentoPaginatedResponse<TData> = {
  items: TData[]
} & MagentoPagination
```

You define the following types:

- `MagentoProduct`: Represents a product in Magento.
- `MagentoAttribute`: Represents an attribute in Magento.
- `MagentoPagination`: Represents the pagination information returned by Magento's API.
- `MagentoPaginatedResponse`: Represents a paginated response from Magento's API for a specific item type, such as products.

Next, add the `getProducts` method to the `MagentoModuleService` class:

```ts title="src/modules/magento/service.ts"
export default class MagentoModuleService {
  // ...
  async getProducts(options?: {
    currentPage?: number
    pageSize?: number
  }): Promise<{
    products: MagentoProduct[]
    attributes: MagentoAttribute[]
    pagination: MagentoPagination
  }> {
    const { currentPage = 1, pageSize = 100 } = options || {}
    const getAccessToken = await this.isAccessTokenExpired()
    if (getAccessToken) {
      await this.authenticate()
    }

    // TODO prepare query params
  }
}
```

The `getProducts` method receives an optional `options` object with the `currentPage` and `pageSize` properties. So far, you check if the access token has expired and, if so, retrieve a new one using the `authenticate` method.

Next, you'll prepare the query parameters to pass in the request that retrieves products. Replace the `TODO` with the following:

```ts title="src/modules/magento/service.ts"
const searchQuery = new URLSearchParams()
// pass pagination parameters
searchQuery.append(
  "searchCriteria[currentPage]", 
  currentPage?.toString() || "1"
)
searchQuery.append(
  "searchCriteria[pageSize]", 
  pageSize?.toString() || "100"
)

// retrieve only configurable products
searchQuery.append(
  "searchCriteria[filter_groups][1][filters][0][field]", 
  "type_id"
)
searchQuery.append(
  "searchCriteria[filter_groups][1][filters][0][value]", 
  "configurable"
)
searchQuery.append(
  "searchCriteria[filter_groups][1][filters][0][condition_type]", 
  "in"
)

// TODO send request to retrieve products
```

You create a `searchQuery` object to store the query parameters to pass in the request. Then, you add the pagination parameters and the filter to retrieve only configurable products.

Next, you'll send the request to retrieve products from Magento. Replace the `TODO` with the following:

```ts title="src/modules/magento/service.ts"
const { items: products, ...pagination }: MagentoPaginatedResponse<MagentoProduct> = await fetch(
  `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/products?${searchQuery}`, 
  {
    headers: {
      "Authorization": `Bearer ${this.accessToken.token}`,
    },
  }
).then((res) => res.json())
.catch((err) => {
  console.log(err)
  throw new MedusaError(
    MedusaError.Types.INVALID_DATA, 
    `Failed to get products from Magento: ${err.message}`
  )
})

// TODO prepare products
```

You send a `GET` request to the Magento server's `/rest/{storeCode}/V1/products` endpoint, passing the query parameters in the URL. You also pass the access token in the `Authorization` header.

Next, you'll prepare the retrieved products by retrieving their children, configurable attributes, and modifying their image URLs. Replace the `TODO` with the following:

```ts title="src/modules/magento/service.ts"
const attributeIds: string[] = []

await promiseAll(
  products.map(async (product) => {
    // retrieve its children
    product.children = await fetch(
      `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/configurable-products/${product.sku}/children`,
      {
        headers: {
          "Authorization": `Bearer ${this.accessToken.token}`,
        },
      }
    ).then((res) => res.json())
    .catch((err) => {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA, 
        `Failed to get product children from Magento: ${err.message}`
      )
    })

    product.media_gallery_entries = product.media_gallery_entries.map(
      (entry) => ({
        ...entry,
        file: `${this.options.migrationOptions?.imageBaseUrl}${entry.file}`,
      }
    ))

    attributeIds.push(...(
      product.extension_attributes.configurable_product_options?.map(
        (option) => option.attribute_id) || []
      )
    )
  })
)

// TODO retrieve attributes
```

You loop over the retrieved products and retrieve their children using the `/rest/{storeCode}/V1/configurable-products/{sku}/children` endpoint. You also modify the image URLs to use the base URL in the migration options, if provided.

In addition, you store the IDs of the configurable products' attributes in the `attributeIds` array. You'll add a method that retrieves these attributes.

Add the new method `getAttributes` to the `MagentoModuleService` class:

```ts title="src/modules/magento/service.ts"
export default class MagentoModuleService {
  // ...
  async getAttributes({
    ids,
  }: {
    ids: string[]
  }): Promise<MagentoAttribute[]> {
    const getAccessToken = await this.isAccessTokenExpired()
    if (getAccessToken) {
      await this.authenticate()
    }

    // filter by attribute IDs
    const searchQuery = new URLSearchParams()
    searchQuery.append(
      "searchCriteria[filter_groups][0][filters][0][field]", 
      "attribute_id"
    )
    searchQuery.append(
      "searchCriteria[filter_groups][0][filters][0][value]", 
      ids.join(",")
    )
    searchQuery.append(
      "searchCriteria[filter_groups][0][filters][0][condition_type]", 
      "in"
    )

    const { 
      items: attributes,
    }: MagentoPaginatedResponse<MagentoAttribute> = await fetch(
      `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/products/attributes?${searchQuery}`, 
      {
        headers: {
          "Authorization": `Bearer ${this.accessToken.token}`,
        },
      }
    ).then((res) => res.json())
    .catch((err) => {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA, 
        `Failed to get attributes from Magento: ${err.message}`
      )
    })

    return attributes
  }
}
```

The `getAttributes` method receives an object with the `ids` property, which is an array of attribute IDs. You check if the access token has expired and, if so, retrieve a new one using the `authenticate` method.

Next, you prepare the query parameters to pass in the request to retrieve attributes. You send a `GET` request to the Magento server's `/rest/{storeCode}/V1/products/attributes` endpoint, passing the query parameters in the URL. You also pass the access token in the `Authorization` header.

Finally, you return the retrieved attributes.

Now, go back to the `getProducts` method and replace the `TODO` with the following:

```ts title="src/modules/magento/service.ts"
const attributes = await this.getAttributes({ ids: attributeIds })
    
return { products, attributes, pagination }
```

You retrieve the configurable products' attributes using the `getAttributes` method and return the products, attributes, and pagination information.

You'll use this method in a later step to retrieve products from Magento.

### Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/magento/index.ts` with the following content:

![Diagram showcasing the module definition file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739348316/Medusa%20Resources/magento-4_bmepvh.jpg)

```ts title="src/modules/magento/index.ts"
import { Module } from "@medusajs/framework/utils"
import MagentoModuleService from "./service"

export const MAGENTO_MODULE = "magento"

export default Module(MAGENTO_MODULE, {
  service: MagentoModuleService,
})
```

You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `magento`.
2. An object with a required property `service` indicating the module's service.

You'll later use the module's service to retrieve products from Magento.

### Pass Options to Plugin

As mentioned earlier when you registered the plugin in the Medusa Application's `medusa-config.ts` file, you can pass options to the plugin. These options are then passed to the modules in the plugin.

So, add the following options to the plugin's registration in the `medusa-config.ts` file of the Medusa application:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  plugins: [
    {
      resolve: "medusa-plugin-magento",
      options: {
        baseUrl: process.env.MAGENTO_BASE_URL,
        username: process.env.MAGENTO_USERNAME,
        password: process.env.MAGENTO_PASSWORD,
        migrationOptions: {
          imageBaseUrl: process.env.MAGENTO_IMAGE_BASE_URL,
        },
      },
    },
  ],
})
```

You pass the options that you defined in the `MagentoModuleService`. Make sure to also set their environment variables in the `.env` file:

```bash
MAGENTO_BASE_URL=https://magento.example.com
MAGENTO_USERNAME=admin
MAGENTO_PASSWORD=password
MAGENTO_IMAGE_BASE_URL=https://magento.example.com/pub/media/catalog/product
```

Where:

- `MAGENTO_BASE_URL`: The base URL of the Magento server. It can also be a local URL, such as `http://localhost:8080`.
- `MAGENTO_USERNAME`: The username of a Magento admin user to authenticate with the Magento server.
- `MAGENTO_PASSWORD`: The password of the Magento admin user.
- `MAGENTO_IMAGE_BASE_URL`: The base URL to use for product images. Magento stores product images in the `pub/media/catalog/product` directory, so you can reference them directly or use a CDN URL. If the URLs of product images in the Medusa server already have a different base URL, you can omit this option.

Medusa supports integrating third-party services, such as [S3](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/s3/index.html.md), in a File Module Provider. Refer to the [File Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/index.html.md) documentation to find other module providers and how to create a custom provider.

You can now use the Magento Module to migrate data, which you'll do in the next steps.

***

## Step 5: Build Product Migration Workflow

In this section, you'll add the feature to migrate products from Magento to Medusa. To implement this feature, you'll use a workflow.

A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an API route or a scheduled job.

By implementing the migration feature in a workflow, you ensure that the data remains consistent and that the migration process can be rolled back if an error occurs.

Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) documentation to learn more about workflows.

### Workflow Steps

The workflow you'll create will have the following steps:

- [getMagentoProductsStep](#getMagentoProductsStep): Retrieve products from Magento using the Magento Module.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve Medusa store details, which you'll need when creating the products.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve a shipping profile, which you'll associate the created products with.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve Magento products that are already in Medusa to update them, instead of creating them.
- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md): Create products in the Medusa application.
- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md): Update existing products in the Medusa application.

You only need to implement the `getMagentoProductsStep` step, which retrieves the products from Magento. The other steps and workflows are provided by Medusa's `@medusajs/medusa/core-flows` package.

### getMagentoProductsStep

The first step of the workflow retrieves and returns the products from Magento.

In your plugin, create the file `src/workflows/steps/get-magento-products.ts` with the following content:

If you get a type error on resolving the Magento Module, run the Medusa application once with the `npm run dev` or `yarn dev` command to generate the necessary type definitions, as explained in the [Automatically Generated Types guide](https://docs.medusajs.com/docs/learn/fundamentals/generated-types/index.html.md).

![Diagram showcasing the get-magento-products file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739349590/Medusa%20Resources/magento-5_ueb4wn.jpg)

```ts title="src/workflows/steps/get-magento-products.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { MAGENTO_MODULE } from "../../modules/magento"
import MagentoModuleService from "../../modules/magento/service"

type GetMagentoProductsInput = {
  currentPage: number
  pageSize: number
}

export const getMagentoProductsStep = createStep(
  "get-magento-products",
  async ({ currentPage, pageSize }: GetMagentoProductsInput, { container }) => {
    const magentoModuleService: MagentoModuleService = 
      container.resolve(MAGENTO_MODULE)

    const response = await magentoModuleService.getProducts({
      currentPage,
      pageSize,
    })

    return new StepResponse(response)
  }
)
```

You create a step using `createStep` from the Workflows SDK. It accepts two parameters:

1. The step's name, which is `get-magento-products`.
2. An async function that executes the step's logic. The function receives two parameters:
   - The input data for the step, which in this case is the pagination parameters.
   - An object holding the workflow's context, including the [Medusa Container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) that allows you to resolve Framework and commerce tools.

In the step function, you resolve the Magento Module's service from the container, then use its `getProducts` method to retrieve the products from Magento.

Steps that return data must return them in a `StepResponse` instance. The `StepResponse` constructor accepts as a parameter the data to return.

### Create migrateProductsFromMagentoWorkflow

You'll now create the workflow that migrates products from Magento using the step you created and steps from Medusa's `@medusajs/medusa/core-flows` package.

In your plugin, create the file `src/workflows/migrate-products-from-magento.ts` with the following content:

![Diagram showcasing the migrate-products-from-magento file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739349820/Medusa%20Resources/magento-6_jjdaxj.jpg)

```ts title="src/workflows/migrate-products-from-magento.ts"
import { 
  createWorkflow, transform, WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  CreateProductWorkflowInputDTO, UpsertProductDTO,
} from "@medusajs/framework/types"
import { 
  createProductsWorkflow, 
  updateProductsWorkflow, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { getMagentoProductsStep } from "./steps/get-magento-products"

type MigrateProductsFromMagentoWorkflowInput = {
  currentPage: number
  pageSize: number
}

export const migrateProductsFromMagentoWorkflowId = 
  "migrate-products-from-magento"

export const migrateProductsFromMagentoWorkflow = createWorkflow(
  {
    name: migrateProductsFromMagentoWorkflowId,
    retentionTime: 10000,
    store: true,
  },
  (input: MigrateProductsFromMagentoWorkflowInput) => {
    const { pagination, products, attributes } = getMagentoProductsStep(
      input
    )
    // TODO prepare data to create and update products
  }
)
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts two parameters:

1. An object with the workflow's configuration, including the name and whether to store the workflow's executions. You enable storing the workflow execution so that you can view it later in the Medusa Admin dashboard.
2. A workflow constructor function, which holds the workflow's implementation. The function receives the input data for the workflow, which is the pagination parameters.

In the workflow constructor function, you use the `getMagentoProductsStep` step to retrieve the products from Magento, passing it the pagination parameters from the workflow's input.

Next, you'll retrieve the Medusa store details and shipping profiles. These are necessary to prepare the data of the products to create or update.

Replace the `TODO` in the workflow function with the following:

```ts title="src/workflows/migrate-products-from-magento.ts"
const { data: stores } = useQueryGraphStep({
  entity: "store",
  fields: ["supported_currencies.*", "default_sales_channel_id"],
  pagination: {
    take: 1,
    skip: 0,
  },
})

const { data: shippingProfiles } = useQueryGraphStep({
  entity: "shipping_profile",
  fields: ["id"],
  pagination: {
    take: 1,
    skip: 0,
  },
}).config({ name: "get-shipping-profiles" })

// TODO retrieve existing products
```

You use the `useQueryGraphStep` step to retrieve the store details and shipping profiles. `useQueryGraphStep` is a Medusa step that wraps [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), allowing you to use it in a workflow. Query is a tool that retrieves data across modules.

When retrieving the store details, you specifically retrieve its supported currencies and default sales channel ID. You'll associate the products with the store's default sales channel, and set their variant prices in the supported currencies. You'll also associate the products with a shipping profile.

Next, you'll retrieve products that were previously migrated from Magento to determine which products to create or update. Replace the `TODO` with the following:

```ts title="src/workflows/migrate-products-from-magento.ts"
const externalIdFilters = transform({
  products,
}, (data) => {
  return data.products.map((product) => product.id.toString())
})

const { data: existingProducts } = useQueryGraphStep({
  entity: "product",
  fields: ["id", "external_id", "variants.id", "variants.metadata"],
  filters: {
    external_id: externalIdFilters,
  },
}).config({ name: "get-existing-products" })

// TODO prepare products to create or update
```

Since the Medusa application creates an internal representation of the workflow's constructor function, you can't manipulate data directly, as variables have no value while creating the internal representation.

Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documentation to learn more about the workflow constructor function's constraints.

Instead, you can manipulate data in a workflow's constructor function using `transform` from the Workflows SDK. `transform` is a function that accepts two parameters:

- The data to transform, which in this case is the Magento products.
- A function that transforms the data. The function receives the data passed in the first parameter and returns the transformed data.

In the transformation function, you return the IDs of the Magento products. Then, you use the `useQueryGraphStep` to retrieve products in the Medusa application that have an `external_id` property matching the IDs of the Magento products. You'll use this property to store the IDs of the products in Magento.

Next, you'll prepare the data to create and update the products. Replace the `TODO` in the workflow function with the following:

```ts title="src/workflows/migrate-products-from-magento.ts" highlights={prepareHighlights}
const { 
  productsToCreate,
  productsToUpdate,
} = transform({
  products,
  attributes,
  stores,
  shippingProfiles,
  existingProducts,
}, (data) => {
  const productsToCreate = new Map<string, CreateProductWorkflowInputDTO>()
  const productsToUpdate = new Map<string, UpsertProductDTO>()

  data.products.forEach((magentoProduct) => {
    const productData: CreateProductWorkflowInputDTO | UpsertProductDTO = {
      title: magentoProduct.name,
      description: magentoProduct.custom_attributes.find(
        (attr) => attr.attribute_code === "description"
      )?.value,
      status: magentoProduct.status === 1 ? "published" : "draft",
      handle: magentoProduct.custom_attributes.find(
        (attr) => attr.attribute_code === "url_key"
      )?.value,
      external_id: magentoProduct.id.toString(),
      thumbnail: magentoProduct.media_gallery_entries.find(
        (entry) => entry.types.includes("thumbnail")
      )?.file,
      sales_channels: [{
        id: data.stores[0].default_sales_channel_id,
      }],
      shipping_profile_id: data.shippingProfiles[0].id,
    }
    const existingProduct = data.existingProducts.find((p) => p.external_id === productData.external_id)

    if (existingProduct) {
      productData.id = existingProduct.id
    }

    productData.options = magentoProduct.extension_attributes.configurable_product_options?.map((option) => {
      const attribute = data.attributes.find((attr) => attr.attribute_id === parseInt(option.attribute_id))
      return {
        title: option.label,
        values: attribute?.options.filter((opt) => {
          return option.values.find((v) => v.value_index === parseInt(opt.value))
        }).map((opt) => opt.label) || [],
      }
    }) || []

    productData.variants = magentoProduct.children?.map((child) => {
      const childOptions: Record<string, string> = {}

      child.custom_attributes.forEach((attr) => {
        const attrData = data.attributes.find((a) => a.attribute_code === attr.attribute_code)
        if (!attrData) {
          return
        }

        childOptions[attrData.default_frontend_label] = attrData.options.find((opt) => opt.value === attr.value)?.label || ""
      })

      const variantExternalId = child.id.toString()
      const existingVariant = existingProduct.variants.find((v) => v.metadata.external_id === variantExternalId)

      return {
        title: child.name,
        sku: child.sku,
        options: childOptions,
        prices: data.stores[0].supported_currencies.map(({ currency_code }) => {
          return {
            amount: child.price,
            currency_code,
          }
        }),
        metadata: {
          external_id: variantExternalId,
        },
        id: existingVariant?.id,
      }
    })

    productData.images = magentoProduct.media_gallery_entries.filter((entry) => !entry.types.includes("thumbnail")).map((entry) => {
      return {
        url: entry.file,
        metadata: {
          external_id: entry.id.toString(),
        },
      }
    })

    if (productData.id) {
      productsToUpdate.set(existingProduct.id, productData)
    } else {
      productsToCreate.set(productData.external_id!, productData)
    }
  })

  return {
    productsToCreate: Array.from(productsToCreate.values()),
    productsToUpdate: Array.from(productsToUpdate.values()),
  }
})

// TODO create and update products
```

You use `transform` again to prepare the data to create and update the products in the Medusa application. For each Magento product, you map its equivalent Medusa product's data:

- You set the product's general details, such as the title, description, status, handle, external ID, and thumbnail using the Magento product's data and custom attributes.
- You associate the product with the default sales channel and shipping profile retrieved previously.
- You map the Magento product's configurable product options to Medusa product options. In Medusa, a product's option has a label, such as "Color", and values, such as "Red". To map the option values, you use the attributes retrieved from Magento.
- You map the Magento product's children to Medusa product variants. For the variant options, you pass an object whose keys is the option's label, such as "Color", and values is the option's value, such as "Red". For the prices, you set the variant's price based on the Magento child's price for every supported currency in the Medusa store. Also, you set the Magento child product's ID in the Medusa variant's `metadata.external_id` property.
- You map the Magento product's media gallery entries to Medusa product images. You filter out the thumbnail image and set the URL and the Magento image's ID in the Medusa image's `metadata.external_id` property.

In addition, you use the existing products retrieved in the previous step to determine whether a product should be created or updated. If there's an existing product whose `external_id` matches the ID of the magento product, you set the existing product's ID in the `id` property of the product to be updated. You also do the same for its variants.

Finally, you return the products to create and update.

The last steps of the workflow is to create and update the products. Replace the `TODO` in the workflow function with the following:

```ts title="src/workflows/migrate-products-from-magento.ts"
createProductsWorkflow.runAsStep({
  input: {
    products: productsToCreate,
  },
})

updateProductsWorkflow.runAsStep({
  input: {
    products: productsToUpdate,
  },
})

return new WorkflowResponse(pagination)
```

You use the `createProductsWorkflow` and `updateProductsWorkflow` workflows from Medusa's `@medusajs/medusa/core-flows` package to create and update the products in the Medusa application.

Workflows must return an instance of `WorkflowResponse`, passing as a parameter the data to return to the workflow's executor. This workflow returns the pagination parameters, allowing you to paginate the product migration process.

You can now use this workflow to migrate products from Magento to Medusa. You'll learn how to use it in the next steps.

***

## Step 6: Schedule Product Migration

There are many ways to execute tasks asynchronously in Medusa, such as [scheduling a job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md) or [handling emitted events](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).

In this guide, you'll learn how to schedule the product migration at a specified interval using a scheduled job. A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime.

Refer to the [Scheduled Jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md) documentation to learn more about scheduled jobs.

To create a scheduled job, in your plugin, create the file `src/jobs/migrate-magento.ts` with the following content:

![Diagram showcasing the migrate-magento file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739358924/Medusa%20Resources/magento-7_rqoodo.jpg)

```ts title="src/jobs/migrate-magento.ts"
import { MedusaContainer } from "@medusajs/framework/types"
import { migrateProductsFromMagentoWorkflow } from "../workflows"

export default async function migrateMagentoJob(
  container: MedusaContainer
) {
  const logger = container.resolve("logger")
    logger.info("Migrating products from Magento...")
    
    let currentPage = 0
    const pageSize = 100
    let totalCount = 0
  
    do {
      currentPage++
  
      const { 
        result: pagination,
      } = await migrateProductsFromMagentoWorkflow(container).run({
        input: {
          currentPage,
          pageSize,
        },
      })
  
      totalCount = pagination.total_count
    } while (currentPage * pageSize < totalCount)
  
    logger.info("Finished migrating products from Magento")
}

export const config = {
  name: "migrate-magento-job",
  schedule: "0 0 * * *",
}
```

A scheduled job file must export:

- An asynchronous function that executes the job's logic. The function receives the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) as a parameter.
- An object with the job's configuration, including the name and the schedule. The schedule is a cron job pattern as a string.

In the job function, you resolve the [logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) from the container to log messages. Then, you paginate the product migration process by running the `migrateProductsFromMagentoWorkflow` workflow at each page until you've migrated all products. You use the pagination result returned by the workflow to determine whether there are more products to migrate.

Based on the job's configurations, the Medusa application will run the job at midnight every day.

### Test it Out

To test out this scheduled job, first, change the configuration to run the job every minute:

```ts title="src/jobs/migrate-magento.ts"
export const config = {
  // ...
  schedule: "* * * * *",
}
```

Then, make sure to run the `plugin:develop` command in the plugin if you haven't already:

```bash
npx medusa plugin:develop
```

This ensures that the plugin's latest changes are reflected in the Medusa application.

Finally, start the Medusa application that the plugin is installed in:

```bash npm2yarn
npm run dev
```

After a minute, you'll see a message in the terminal indicating that the migration started:

```plain title="Terminal"
info: Migrating products from Magento...
```

Once the migration is done, you'll see the following message:

```plain title="Terminal"
info: Finished migrating products from Magento
```

To confirm that the products were migrated, open the Medusa Admin dashboard at `http://localhost:9000/app` and log in. Then, click on Products in the sidebar. You'll see your magento products in the list of products.

![Click on products at the sidebar on the right, then view the products in the table in the middle.](https://res.cloudinary.com/dza7lstvk/image/upload/v1739359394/Medusa%20Resources/Screenshot_2025-02-12_at_1.22.44_PM_uva98i.png)

***

## Next Steps

You've now implemented the logic to migrate products from Magento to Medusa. You can re-use the plugin across Medusa applications. You can also expand on the plugin to:

- Migrate other entities, such as orders, customers, and categories. Migrating other entities follows the same pattern as migrating products, using workflows and scheduled jobs. You only need to format the data to be migrated as needed.
- Allow triggering migrations from the Medusa Admin dashboard using [Admin Customizations](https://docs.medusajs.com/docs/learn/fundamentals/admin/index.html.md). This feature is available in the [Example Repository](https://github.com/medusajs/example-repository/tree/main/src/admin).

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Add Newsletter Subscriptions with Mailchimp in Medusa

In this tutorial, you'll learn how to integrate Mailchimp with Medusa to manage newsletter subscribers and automate newsletters.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. Medusa's architecture facilitates integrating third-party services to customize Medusa's infrastructure for your business needs.

Medusa's [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) allows you to customize Medusa's infrastructure to send notifications using the third-party provider that fits your business needs, such as [Mailchimp](https://mailchimp.com/).

In this tutorial, you'll integrate Mailchimp with Medusa to allow customers to subscribe to your newsletter and automate sending newsletters.

## Summary

By following this tutorial, you'll learn how to:

- Install and set up Medusa.
- Integrate Mailchimp with Medusa.
- Allow customers to subscribe to your store's newsletter.
- Automate sending newsletters about new products to subscribed customers.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Diagram showcasing the flow of the Mailchimp integration with Medusa](https://res.cloudinary.com/dza7lstvk/image/upload/v1750679344/Medusa%20Resources/mailchimp-overview_itmiul.jpg)

[Example Repository](https://github.com/medusajs/examples/tree/main/mailchimp-integration): Find the full code of the guide in this repository.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

First, you'll be asked for the project's name. Then, when prompted about installing the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Mailchimp Module Provider

To integrate third-party services into Medusa, you create a custom [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a reusable package with functionalities related to a single feature or domain.

Medusa's [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) provides an interface to send notifications in your Medusa application. It delegates the actual sending of notifications to the underlying provider, such as Mailchimp.

In this step, you'll integrate Mailchimp as a Notification Module Provider. Later, you'll use it to handle newsletter subscriptions and send newsletters.

Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules in Medusa.

### a. Install Mailchimp Marketing API SDK

To interact with Mailchimp's APIs, you'll use their official Node.js SDK.

Run the following command to install the SDK with its types package in your Medusa application:

```bash npm2yarn
npm install @mailchimp/mailchimp_marketing
npm install @types/mailchimp__mailchimp_marketing --save-dev
```

### b. Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/mailchimp`.

### c. Create Mailchimp Module's Service

A module has a service that contains its logic. For Notification Module Providers, the service implements the logic to send notifications with a third-party service.

To create the service of the Mailchimp Notification Module Provider, create the file `src/modules/mailchimp/service.ts` with the following content:

```ts title="src/modules/mailchimp/service.ts" highlights={serviceHighlights}
import { 
  AbstractNotificationProviderService, 
} from "@medusajs/framework/utils"
import mailchimpMarketingApi from "@mailchimp/mailchimp_marketing"

type Options = {
  apiKey: string
  server: string
  listId: string
  templates?: {
    new_products?: {
      subject_line?: string
      storefront_url?: string
    }
  }
}

type InjectedDependencies = {
}

class MailchimpNotificationProviderService extends AbstractNotificationProviderService {
  static identifier = "mailchimp"
  protected options: Options
  protected mailchimp: typeof mailchimpMarketingApi

  constructor(container: InjectedDependencies, options: Options) {
    super()
    this.options = options
    this.mailchimp = mailchimpMarketingApi
    this.mailchimp.setConfig({
      apiKey: options.apiKey,
      server: options.server,
    })
  }
}

export default MailchimpNotificationProviderService
```

A Notification Module Provider's service must extend the `AbstractNotificationProviderService` class. You'll implement its methods in the next sections.

The service must also have an `identifier` static property, which is a unique identifier for the module. This identifier is used when registering the module in the Medusa application.

The service's constructor receives two parameters:

- `container`: The [module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) that contains Framework resources available to the module. You don't need to use it for this tutorial.
- `options`: Options that are passed to the module provider when it's registered in Medusa's configurations. You define the following options:
  - `apiKey`: The Mailchimp API key.
  - `server`: The Mailchimp server prefix. For example, `us10`.
  - `listId`: The ID of the Mailchimp audience list where subscribed customers will be added.
  - `templates`: Optional template configurations for newsletters.

You'll learn how to set these options in the [Add Module Provider to Medusa's Configurations](#g-add-module-provider-to-medusas-configurations) section.

In the constructor, you set the `options` property and initialize the Mailchimp SDK with your API key and server.

In the next sections, you'll implement the methods of the `MailchimpNotificationProviderService` class.

### d. Implement validateOptions Method

The `validateOptions` method is used to validate the options passed to the module provider. If the method throws an error, the Medusa application won't start.

Add the `validateOptions` method to the `MailchimpNotificationProviderService` class:

```ts title="src/modules/mailchimp/service.ts"
// other imports...
import { 
  MedusaError,
} from "@medusajs/framework/utils"

class MailchimpNotificationProviderService extends AbstractNotificationProviderService {
  // ...
  static validateOptions(options: Record<any, any>): void | never {
    if (!options.apiKey) {
      throw new MedusaError(
        MedusaError.Types.INVALID_ARGUMENT, 
        "API key is required"
      )
    }
    if (!options.server) {
      throw new MedusaError(
        MedusaError.Types.INVALID_ARGUMENT, 
        "Server is required"
      )
    }
    if (!options.listId) {
      throw new MedusaError(
        MedusaError.Types.INVALID_ARGUMENT, 
        "List ID is required"
      )
    }
  }
}
```

The `validateOptions` method receives the options passed to the module provider as a parameter.

In the method, you throw an error if the required options are not set.

### e. Implement send Method

When the Medusa application needs to send a notification through a channel (such as `email`), it calls the `send` method of the channel's module provider.

The `send` method can be used to send different types of notifications based on the template specified. So, you'll implement the helper methods that handle the different notification templates, then use them in the `send` method.

#### sendNewsletterSignup Method

The `sendNewsletterSignup` method adds an email to the Mailchimp audience list. You'll use this method when a customer subscribes to the newsletter.

Add the `sendNewsletterSignup` method to the `MailchimpNotificationProviderService` class:

```ts title="src/modules/mailchimp/service.ts"
// other imports...
import { 
  ProviderSendNotificationResultsDTO, 
  ProviderSendNotificationDTO,
} from "@medusajs/framework/types"

class MailchimpNotificationProviderService extends AbstractNotificationProviderService {
  // ...
  async sendNewsletterSignup(
    notification: ProviderSendNotificationDTO
  ): Promise<ProviderSendNotificationResultsDTO> {
    const { to, data } = notification

    try {
      const response = await this.mailchimp.lists.addListMember(
        this.options.listId, {
          email_address: to,
          status: "subscribed",
          merge_fields: {
            FNAME: data?.first_name,
            LNAME: data?.last_name,
          },
        }
      )
  
      return {
        id: "id" in response ? response.id : "",
      }
    } catch (error) {
      throw new MedusaError(
        MedusaError.Types.UNEXPECTED_STATE, 
        `Failed to send newsletter signup: ${error.response.text}`
      )
    }
  }
}
```

This method receives the same parameter as the `send` method, which is an object containing the notification details including:

- `to`: The email address to add to the Mailchimp audience list.
- `data`: An object containing additional data, such as the user's first and last name.

Learn about other properties in the object in the [Create Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module#send/index.html.md) guide.

In the method, you use the `mailchimp.lists.addListMember` method to subscribe an email address to the Mailchimp audience list. You pass the `listId` from the module's options and the email address along with optional first and last names.

If the subscription is successful, the method returns an object with the `id` of the subscribed email. If it fails, it throws a `MedusaError` with the error message from Mailchimp.

You don't do any email sending in this method because later, in the [Send Welcome Emails from Mailchimp](#optional-step-send-welcome-emails-from-mailchimp) section, you'll create an automation flow in Mailchimp that automatically sends a welcome email to new subscribers.

#### getNewProductsHtmlContent Method

The `getNewProductsHtmlContent` method will generate the HTML content for the "New Products" newsletter. You'll then use this method when sending the new products newsletter.

Add the `getNewProductsHtmlContent` method to the `MailchimpNotificationProviderService` class:

```ts title="src/modules/mailchimp/service.ts"
class MailchimpNotificationProviderService extends AbstractNotificationProviderService {
  // ...
  private async getNewProductsHtmlContent(data: any): Promise<string> {
    return `
      <!DOCTYPE html>
        <html xmlns:mc="http://schemas.mailchimp.com/2006/hcm">
        <head>
          <meta charset="UTF-8">
          <title>${this.options.templates?.new_products?.subject_line}</title>
          <style>
            body {
              font-family: Arial, sans-serif;
              background-color: #f4f4f4;
              margin: 0;
              padding: 0;
            }
            .container {
              max-width: 600px;
              background: #ffffff;
              margin: 0 auto;
              padding: 20px;
            }
            .product {
              border-bottom: 1px solid #ddd;
              padding: 20px 0;
              display: flex;
            }
            .product img {
              max-width: 120px;
              margin-right: 20px;
            }
            .product-info {
              flex: 1;
            }
            .product-info h4 {
              margin: 0 0 10px;
              font-size: 18px;
            }
            .product-info p {
              margin: 0 0 5px;
              color: #555;
            }
            .cta-button {
              display: inline-block;
              margin-top: 10px;
              background-color: #007BFF;
              color: #ffffff;
              text-decoration: none;
              padding: 10px 15px;
              border-radius: 4px;
              font-size: 14px;
            }
          </style>
        </head>
        <body>
          <div class="container">
            <h2 style="text-align:center;">Check out our latest products</h2>

            <!-- Repeatable product block -->
            <table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" mc:repeatable="product_block" mc:variant="Product Item">
              <tbody>
                ${data.products.map((product: any) => `
                  <tr>
                    <td class="product">
                        <img mc:edit="product_image" src="${product.thumbnail}" alt="Product Image">
                        <div class="product-info">
                          <h4 mc:edit="product_title">${product.title}</h4>
                          <p mc:edit="product_description">${product.description}</p>
                          <a mc:edit="product_link" href="${this.options.templates?.new_products?.storefront_url}/products/${product.handle}" class="cta-button">View Product</a>
                        </div>
                    </td>
                  </tr>
                `).join("")}
              </tbody>
            </table>

          </div>
        </body>
        </html>
    `
  }
}
```

This method receives the product data as a parameter and returns a string containing the HTML content for the newsletter. You show the products in a responsive layout with images, titles, descriptions, and a button to view the product.

Notice that the HTML template uses the `template.new_products.storefront_url` module option to generate the product links. This allows you to customize the storefront URL in the module's options.

Feel free to modify the HTML template to match your design preferences or add more product details. You can also define a template in Mailchimp and use its ID in the next method instead of generating the HTML content dynamically.

#### sendNewProducts Method

The last helper method you'll add is `sendNewProducts`. This method will create and send a campaign in Mailchimp that showcases new products.

Add the `sendNewProducts` method to the `MailchimpNotificationProviderService` class:

```ts title="src/modules/mailchimp/service.ts" highlights={sendNewProductsHighlights}
class MailchimpNotificationProviderService extends AbstractNotificationProviderService {
  // ...
  async sendNewProducts(
    notification: ProviderSendNotificationDTO
  ): Promise<ProviderSendNotificationResultsDTO> {
    const { data } = notification

    try {
      const list = await fetch(
        `https://${this.options.server}.api.mailchimp.com/3.0/lists/${this.options.listId}`, 
        {
          headers: {
            Authorization: `Bearer ${this.options.apiKey}`,
          },
        }
      ).then((res) => res.json()) as mailchimpMarketingApi.lists.List

      // create a campaign
      const campaign = await this.mailchimp.campaigns.create({
        type: "regular",
        recipients: {
          list_id: this.options.listId,
        },
        settings: {
          subject_line: 
            this.options.templates?.new_products?.subject_line || "New Products",
          from_name: list.campaign_defaults.from_name,
          reply_to: list.campaign_defaults.from_email,
        },
      }) as mailchimpMarketingApi.campaigns.Campaigns

      // set content
      await this.mailchimp.campaigns.setContent(campaign.id, {
        html: await this.getNewProductsHtmlContent(data),
      })

      // send campaign
      await this.mailchimp.campaigns.send(campaign.id)

      return {
        id: campaign.id,
      }
    } catch (error) {
      throw new MedusaError(
        MedusaError.Types.UNEXPECTED_STATE, 
        `Failed to send new products newsletter: ${
          error.response?.text || error
        }`
      )
    }
  }
}
```

This method receives the same parameter as the `send` method, which is an object containing the notification details.

Learn about the object's properties in the [Create Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module#send/index.html.md) guide.

In the method, you:

1. Fetch the Mailchimp list details to get default sender information. You use the `fetch` API because the Mailchimp SDK does not provide a method to fetch list details.
2. Create a new campaign in Mailchimp using the `mailchimp.campaigns.create` method. You specify the list ID to ensure the subscribers of that list receive the campaign. You also set the subject line and sender information using the list's default settings.
3. Set the campaign content using the HTML returned by the `getNewProductsHtmlContent` method.
4. Send the campaign using the `mailchimp.campaigns.send` method.
5. Return the campaign ID if successful.

You also throw a `MedusaError` if the method fails at any point, providing the error message from Mailchimp.

#### Implement send Method

You can now implement the required `send` method of the `MailchimpNotificationProviderService` class that sends a notification based on the template provided.

Add the `send` method to the `MailchimpNotificationProviderService` class:

```ts title="src/modules/mailchimp/service.ts"
class MailchimpNotificationProviderService extends AbstractNotificationProviderService {
  // ...
  async send(
    notification: ProviderSendNotificationDTO
  ): Promise<ProviderSendNotificationResultsDTO> {
    const { template } = notification

    switch (template) {
      case "newsletter-signup":
        return this.sendNewsletterSignup(notification)
      case "new-products":
        return this.sendNewProducts(notification)
      default:
        throw new MedusaError(
          MedusaError.Types.INVALID_ARGUMENT, 
          "Invalid template"
        )
    }
  }
}
```

This method receives an object of notification details, including the `template` property that specifies which template to use for sending the notification.

Learn about other properties in the object in the [Create Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module#send/index.html.md) guide.

In the method, you perform an action based on the `template` value:

- If the template is `newsletter-signup`, you call the `sendNewsletterSignup` method to add a new subscriber to the Mailchimp audience list.
- If the template is `new-products`, you call the `sendNewProducts` method to create and send a campaign showcasing new products.
- If the template is not recognized, you throw a `MedusaError` indicating that the template is invalid.

### f. Export Module Definition

You've now finished implementing the necessary methods for the Mailchimp Notification Module Provider.

The final piece to a module is its definition, which you export in an `index.ts` file at the module's root directory. This definition tells Medusa the module's details, including its service.

To create the module's definition, create the file `src/modules/mailchimp/index.ts` with the following content:

```ts title="src/modules/mailchimp/index.ts"
import MailchimpNotificationProviderService from "./service"
import { 
  ModuleProvider, 
  Modules,
} from "@medusajs/framework/utils"

export default ModuleProvider(Modules.NOTIFICATION, {
  services: [MailchimpNotificationProviderService],
})
```

You use `ModuleProvider` from the Modules SDK to create the module provider's definition. It accepts two parameters:

1. The name of the module that this provider belongs to, which is `Modules.NOTIFICATION` in this case.
2. An object with a required property `services` indicating the Module Provider's services.

### g. Add Module Provider to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property to the configurations:

```ts title="medusa-config.ts" highlights={configurationsHighlight}
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          {
            resolve: "./src/modules/mailchimp",
            id: "mailchimp",
            options: {
              channels: ["email"],
              apiKey: process.env.MAILCHIMP_API_KEY!,
              server: process.env.MAILCHIMP_SERVER!,
              listId: process.env.MAILCHIMP_LIST_ID!,
              templates: {
                new_products: {
                  subject_line: process.env.MAILCHIMP_NEW_PRODUCTS_SUBJECT_LINE!,
                  storefront_url: process.env.MAILCHIMP_NEW_PRODUCTS_STOREFRONT_URL!,
                },
              },
            },
          },
        ],
      },
    },
  ],
})
```

To pass a Module Provider to the Notification Module, you add the `modules` property to the Medusa configuration and pass the Notification Module in its value.

The Notification Module accepts a `providers` option, which is an array of Notification Module Providers to register.

To register the Mailchimp Notification Module Provider, you add an object to the `providers` array with the following properties:

- `resolve`: The NPM package or path to the module provider. In this case, it's the path to the `src/modules/mailchimp` directory.
- `id`: The ID of the module provider. The Notification Module Provider is then registered with the ID `np_{identifier}_{id}`, where:
  - `{identifier}`: The identifier static property defined in the Module Provider's service, which is `mailchimp` in this case.
  - `{id}`: The ID set in this configuration, which is also `mailchimp` in this case.
- `options`: The options to pass to the module provider. These are the options you defined in the `Options` type of the module provider's service.
  - You must also set a `channels` option that indicates the channels this provider is used to send notifications.

### h. Set Environment Variables

Next, you'll set the options you passed to the Mailchimp Notification Module Provider as environment variables.

#### Retrieve Mailchimp API Key

To retrieve your Mailchimp API key:

1. On the Mailchimp dashboard, click on your profile icon at the top right.
2. Choose "Account & billing" from the dropdown.

![Mailchimp dashboard with the profile menu opened](https://res.cloudinary.com/dza7lstvk/image/upload/v1750682692/Medusa%20Resources/CleanShot_2025-06-23_at_11.46.07_2x_xo5a5l.png)

3. Click on the "Extras" tab and select "API keys" from the dropdown.

![Mailchimp account page with the Extras dropdown opened](https://res.cloudinary.com/dza7lstvk/image/upload/v1750682741/Medusa%20Resources/CleanShot_2025-06-23_at_11.48.12_2x_l86ein.png)

4. Scroll down to the "Your API keys" section and click on the "Create A Key" button.

![Mailchimp API keys page with the Create A Key button highlighted](https://res.cloudinary.com/dza7lstvk/image/upload/v1750682771/Medusa%20Resources/CleanShot_2025-06-23_at_11.48.53_2x_vf3ftl.png)

5. In the API key form, enter a name for the API key.
6. Click the "Generate Key" button to create the API key.

![Mailchimp API key creation form with a name field](https://res.cloudinary.com/dza7lstvk/image/upload/v1750682817/Medusa%20Resources/CleanShot_2025-06-23_at_11.49.12_2x_xwwmi6.png)

Copy the generated API key and add it to the `.env` file in your Medusa application:

```shell
MAILCHIMP_API_KEY=123...
```

#### Retrieve Mailchimp Server Prefix

You can retrieve your Mailchimp server prefix from the URL of your Mailchimp dashboard. It should be in the format `https://<server-prefix>.admin.mailchimp.com/`. So, the server prefix will be something like `us5`, for example.

Then, add the server prefix to the `.env` file in your Medusa application:

```shell
MAILCHIMP_SERVER=us5
```

#### Create Mailchimp Audience List

Next, you'll create a Mailchimp audience list to store your subscribers:

1. On the Mailchimp dashboard, click on "Audience" in the left sidebar.
2. Click on the dropdown next to the "Contacts" header and choose "Manage Audiences".

![Mailchimp audience management page with the Manage Audiences option highlighted](https://res.cloudinary.com/dza7lstvk/image/upload/v1750683058/Medusa%20Resources/CleanShot_2025-06-23_at_11.50.54_2x_ayyyuq.png)

3. Click on the "Create Audience" button at the top right.

![Mailchimp audience creation page with the Create Audience button highlighted](https://res.cloudinary.com/dza7lstvk/image/upload/v1750683095/Medusa%20Resources/CleanShot_2025-06-23_at_11.51.51_2x_d0nxvc.png)

4. Enter the audience details, such as the audience name, default from email address, and default from name. These defaults are used in the created campaigns.

![Mailchimp audience creation form with fields for audience name, default from email address, and default from name](https://res.cloudinary.com/dza7lstvk/image/upload/v1750683517/Medusa%20Resources/CleanShot_2025-06-23_at_11.54.25_2x_ihgiyt.png)

5. Once you're done, click the "Save" button to create the audience. This will open the Audience's contacts page.
6. Click on the "More options" button and select "Audience settings" from the dropdown.

![Mailchimp audience contacts page with the More options button highlighted](https://res.cloudinary.com/dza7lstvk/image/upload/v1750683642/Medusa%20Resources/CleanShot_2025-06-23_at_16.00.19_2x_zjetg6.png)

7. In the Audience settings page, copy the value of "Audience ID" from the first section.

![Mailchimp audience settings page with the Audience ID highlighted](https://res.cloudinary.com/dza7lstvk/image/upload/v1750683763/Medusa%20Resources/CleanShot_2025-06-23_at_16.01.51_2x_a8grlw.png)

Add the copied ID to the `.env` file in your Medusa application as the list ID:

```shell
MAILCHIMP_LIST_ID=123...
```

#### Set Mailchimp Templates Options

Finally, you can optionally set the Mailchimp templates options in the `.env` file. These options are used when sending newsletters about new products.

```shell
MAILCHIMP_NEW_PRODUCTS_SUBJECT_LINE="Check out our new products!"
MAILCHIMP_NEW_PRODUCTS_STOREFRONT_URL=https://localhost:8000
```

Where:

- `MAILCHIMP_NEW_PRODUCTS_SUBJECT_LINE`: The subject line for the new products newsletter.
- `MAILCHIMP_NEW_PRODUCTS_STOREFRONT_URL`: The URL of your storefront where users can view the new products. In development, the Next.js Starter Storefront runs on `http://localhost:8000`, so you can set it to that URL.

The Mailchimp integration is now ready. You'll test it out as you implement the subscription features in the next steps.

***

## Optional Step: Send Welcome Emails from Mailchimp

In the Mailchimp Notification Module Provider, you handle the `newsletter-signup` notification template by subscribing an email address to the Mailchimp audience list. However, you typically should also send a welcome email to the subscribed customer.

To do that, you can create an automation flow in Mailchimp that automatically sends emails whenever a new subscriber is added to the audience list.

To do that:

1. On the Mailchimp dashboard, click on "Automations" in the sidebar.
2. Click on the "Build from scratch" button to create a new automation flow.

![Mailchimp automations page with the Build from scratch button highlighted](https://res.cloudinary.com/dza7lstvk/image/upload/v1750684308/Medusa%20Resources/CleanShot_2025-06-23_at_12.18.15_2x_ymj8ar.png)

3. In the flow creation form, enter a name for the automation flow and choose the Audience you created earlier.
4. Click the "Choose a starting point" button to select a template for the automation flow.

![Mailchimp automation flow creation form with fields for flow name and audience selection](https://res.cloudinary.com/dza7lstvk/image/upload/v1750684445/Medusa%20Resources/CleanShot_2025-06-23_at_12.19.17_2x_yso5wu.png)

5. A pop-up will open in the flow editor to choose a template to start from. Choose the "Signs up for Email" template.

![Mailchimp automation flow editor with the Signs up for Email template highlighted](https://res.cloudinary.com/dza7lstvk/image/upload/v1750685997/Medusa%20Resources/CleanShot_2025-06-23_at_12.19.32_2x_x14tcv.png)

6. In the flow editor, drag the "Send Email" action to the flow canvas. This will open a pop-up to configure the email.
7. In the email configuration pop-up, you can enter the email subject, from name, and from email address.

![Mailchimp automation flow editor with the Send Email action highlighted](https://res.cloudinary.com/dza7lstvk/image/upload/v1750686062/Medusa%20Resources/CleanShot_2025-06-23_at_12.20.56_2x_iksdox.png)

8. To set the email content, click the "Select a template" link in the email configuration pop-up. You can then choose an existing template or paste your own HTML content.

### Example HTML Content

```html
<!DOCTYPE html>
<html xmlns:mc="http://schemas.mailchimp.com/2006/hcm">
<head>
  <meta charset="UTF-8">
  <title>Thanks for signing up!</title>
  <style type="text/css">
    body {
      background-color: #f5f5f5;
      font-family: Arial, sans-serif;
      margin: 0;
      padding: 0;
    }
    .email-container {
      max-width: 600px;
      margin: 40px auto;
      background-color: #ffffff;
      padding: 30px;
      border-radius: 8px;
      box-shadow: 0 0 5px rgba(0,0,0,0.1);
    }
    h1 {
      color: #333;
      font-size: 24px;
      margin-bottom: 20px;
    }
    p {
      color: #555;
      line-height: 1.6;
    }
    .cta-button {
      display: inline-block;
      margin-top: 20px;
      padding: 12px 24px;
      background-color: #007BFF;
      color: #ffffff;
      text-decoration: none;
      border-radius: 5px;
      font-weight: bold;
    }
    .footer {
      text-align: center;
      font-size: 12px;
      color: #999;
      margin-top: 30px;
    }
  </style>
</head>
<body>
  <div class="email-container">
    <h1 mc:edit="welcome_heading">Thank you for signing up to the Medusa newsletter 🎉</h1>
    <p mc:edit="welcome_text">
      Hi there,<br><br>
      Thanks for signing up! Get ready for exciting product updates and special offers sent right to your inbox.
    </p>

    <a href="http://localhost:8000" class="cta-button" mc:edit="cta_button">Start Exploring</a>

    <div class="footer" mc:edit="footer_text">
      You’re receiving this email because you signed up for updates from Medusa.<br>
      Want to unsubscribe? <a href="*|UNSUB|*">Click here</a>.
    </div>
  </div>
</body>
</html>
```

9. Once you're done, close the email configuration pop-up. The changes will be saved automatically.
10. In the flow editor, click the "Continue" button at the top right.

![Mailchimp automation flow editor with the Continue button highlighted](https://res.cloudinary.com/dza7lstvk/image/upload/v1750686278/Medusa%20Resources/CleanShot_2025-06-23_at_12.24.15_2x_v1mcas.png)

11. In the review pop-up, click the "Turn flow on" button to activate the automation flow.

![Mailchimp automation flow review pop-up with the Turn flow on button highlighted](https://res.cloudinary.com/dza7lstvk/image/upload/v1750686313/Medusa%20Resources/CleanShot_2025-06-23_at_12.24.36_2x_wji8xx.png)

Whenever a customer subscribes to the newsletter, Mailchimp will automatically send them a welcome email using the automation flow you created.

***

## Step 3: Create Newsletter Subscription API Route

Now that you've integrated Mailchimp with Medusa, you need to allow customers to subscribe to the newsletter.

In this step, you will:

- Create an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) to subscribe customers. An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts.
  - In the API route, you'll emit an event indicating that the customer is signing up for the newsletter.
- Create a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that listens to the event emitted by the API route. The subscriber will use Mailchimp to subscribe the customer to the newsletter audience list.

### a. Create the API Route

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

So, to create an API route at the path `/store/newsletter`, create the file `src/api/store/newsletter/route.ts` with the following content:

```ts title="src/api/store/newsletter/route.ts" highlights={apiRouteHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { z } from "zod"

export const newsletterSignupSchema = z.object({
  email: z.string().email(),
  first_name: z.string().optional(),
  last_name: z.string().optional(),
})

export async function POST(
  req: MedusaRequest<z.infer<typeof newsletterSignupSchema>>,
  res: MedusaResponse
) {
  const eventModuleService = req.scope.resolve("event_bus")

  await eventModuleService.emit({
    name: "newsletter.signup",
    data: {
      email: req.validatedBody.email,
      first_name: req.validatedBody.first_name,
      last_name: req.validatedBody.last_name,
    },
  })

  res.json({
    success: true,
  })
}
```

You first export a [Zod](https://zod.dev/) schema object that you'll use to validate incoming request bodies. You expect the request body to have an `email` field, and optionally allow passing `first_name` and `last_name` fields.

Then, you export a `POST` route handler function. This will expose a `POST` API route at `/store/newsletter`. The route handler function accepts two parameters:

1. A request object with details and context on the request, such as body parameters.
2. A response object to manipulate and send the response.

In the route handler, you use the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) to resolve the [Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/index.html.md)'s service.

Then, you emit the `newsletter.signup` event, passing as the event payload the email, first name, and last name from the request body.

Finally, you send a JSON response indicating that the request was successful.

### b. Add Validation Middleware

To validate that requests sent to the `/store/newsletter` API route have the required body parameters, you'll add a validation [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md). A middleware is a function that is executed before an API route's handler when a request is made to the route.

To apply the validation middleware on the `/store/newsletter` API route, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares, 
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { newsletterSignupSchema } from "./store/newsletter/route"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/store/newsletter",
      methods: ["POST"],
      middlewares: [
        validateAndTransformBody(newsletterSignupSchema),
      ],
    },
  ],
})
```

You define middlewares using the `defineMiddlewares` function from the Medusa Framework. It accepts an object having a `routes` property, whose value is an array of middleware route objects. Each middleware route object has the following properties:

- `matcher`: The path of the route the middleware applies to.
- `methods`: The HTTP methods the middleware applies to, which is in this case `POST`.
- `middlewares`: An array of middleware functions to apply to the route. You apply the `validateAndTransformBody` middleware which ensures that a request's body has the parameters required by a route. You pass it the schema you defined earlier in the API route's file.

### c. Create Subscriber

Finally, you'll create a subscriber that listens to the `newsletter.signup` event emitted by the API route. The subscriber will create a notification with the `newsletter-signup` template. Under the hoode, the Mailchimp Notification Module Provider will handle the notification and subscribe the customer to the newsletter audience list.

Create the file `src/subscribers/newsletter-signup.ts` with the following content:

```ts title="src/subscribers/newsletter-signup.ts"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" 

export default async function orderPlacedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ email: string, first_name: string, last_name: string }>) {
  const notificationModuleService = container.resolve("notification")

  await notificationModuleService.createNotifications({
    channel: "email",
    to: data.email,
    template: "newsletter-signup",
    data: {
      first_name: data.first_name,
      last_name: data.last_name,
    },
  })
}

export const config: SubscriberConfig = {
  event: `newsletter.signup`,
}
```

A subscriber file must export:

1. An asynchronous function, which is the subscriber that is executed when the event is emitted.
2. A configuration object that holds the name of the event the subscriber listens to, which is `newsletter.signup` in this case.

The subscriber function receives an object as a parameter that has a `container` property, which is the Medusa container. The Medusa container holds Framework and commerce tools that you can resolve and use in your customizations.

In the subscriber function, you resolve the [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md)'s service from the Medusa container. Then, you call the `createNotifications` method to create a notification with the following properties:

- `channel`: The channel to send the notification through, which is `email` in this case. Since the [Mailchimp Notification Module Provider is registered with the email channel](#g-add-module-provider-to-medusas-configurations), it will process the notification.
- `to`: The email address to subscribe to the newsletter.
- `template`: The template to use for the notification, which is `newsletter-signup`.
- `data`: An object containing additional data to pass to the notification, such as the user's first and last names.

Now you have an API route that allows customers to subscribe to the newsletter. You'll test it when you customize the storefront in the next step.

***

## Step 4: Add Newsletter Subscription Form in the Storefront

In this step, you'll customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) that you installed as part of the first step. You'll add a newsletter subscription form in the storefront's footer that allows customers to subscribe to the newsletter.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-newsletter`, you can find the storefront by going back to the parent directory and changing to the `medusa-newsletter-storefront` directory:

```bash
cd ../medusa-newsletter-storefront # change based on your project name
```

### a. Add Subscribe to Newsletter Function

You'll start by adding a server function that sends a request to the `/store/newsletter` API route you created in the previous step to subscribe a customer to the newsletter.

Create the file `src/lib/data/newsletter.ts` with the following content:

```ts title="src/lib/data/newsletter.ts" badgeLabel="Storefront" badgeColor="blue"
"use server"

import { sdk } from "@lib/config"

export const subscribeToNewsletter = async (email: string) => {
  const response = await sdk.client.fetch(`/store/newsletter`, {
    method: "POST",
    body: {
      email,
    },
  })

  return response
}
```

You use the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), which is already configured in the Next.js Starter Storefront, to send a `POST` request to the `/store/newsletter` API route. You pass the email address in the request body.

### b. Create Newsletter Subscription Form

Next, you'll create a newsletter subscription form component that allows customers to enter their email address and subscribe to the newsletter.

Create the file `src/modules/layout/components/newsletter-form/index.tsx` with the following content:

```tsx title="src/modules/layout/components/newsletter-form/index.tsx" badgeLabel="Storefront" badgeColor="blue"
"use client"

import { subscribeToNewsletter } from "@lib/data/newsletter"
import { SubmitButton } from "@modules/checkout/components/submit-button"
import Input from "@modules/common/components/input"
import { useState } from "react"
import { toast } from "@medusajs/ui"

const NewsletterForm = () => {
  const [email, setEmail] = useState("")
  const [loading, setLoading] = useState(false)

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    setLoading(true)
    try {
      await subscribeToNewsletter(email)
    } catch (error) {
      console.error(error)
      // ignore, don't show error to user
    }
    toast.success("Thanks for subscribing!")
    setEmail("")
    setLoading(false)
  }

  return (
    <div className="flex flex-col lg:flex-row items-center justify-between gap-8">
      <div className="flex-1 max-w-md">
        <h3 className="txt-compact-large-plus mb-2 text-ui-fg-base">Subscribe to our newsletter</h3>
        <p className="text-base-regular text-ui-fg-subtle">
          Receive updates on our latest products and exclusive offers.
        </p>
      </div>
      <div className="flex-1 max-w-md w-full">
        <form onSubmit={handleSubmit} className="flex gap-x-2">
          <div className="flex-1">
            <Input
              label="Email"
              name="email"
              type="email"
              autoComplete="off"
              required
              data-testid="newsletter-email-input"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              disabled={loading}
            />
          </div>
          <div className="flex items-end">
            <SubmitButton data-testid="newsletter-submit-button">
              Subscribe
            </SubmitButton>
          </div>
        </form>
      </div>
    </div>
  )
}

export default NewsletterForm
```

You create a `NewsletterForm` component that renders a form with an email input and a submit button.

Once the customer enters their email and submits the form, you use the `subscribeToNewsletter` function you created to subscribe the customer to the newsletter.

You also show a toast notification to the customer indicating that they successfully subscribed to the newsletter.

The `toast` function is imported from [Medusa UI](https://docs.medusajs.com/ui/index.html.md), which requires adding `Toaster` component in the application's tree. So, import the `Toaster` component in the file `src/app/[countryCode]/(main)/layout.tsx`:

```tsx title="src/app/[countryCode]/(main)/layout.tsx" badgeLabel="Storefront" badgeColor="blue"
import { Toaster } from "@medusajs/ui"
```

Then, in the `PageLayout` component's return statement, add the `Toaster` component after the `Footer` component:

```tsx title="src/app/[countryCode]/(main)/layout.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
    <>
    {/* ... */}
    <Footer />
    <Toaster />
  </>
)
```

### c. Add Newsletter Form to the Footer

Finally, you'll add the `NewsletterForm` component to the storefront's footer.

In the file `src/modules/layout/components/footer/index.tsx`, import the `NewsletterForm` component:

```tsx title="src/modules/layout/components/footer/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import NewsletterForm from "@modules/layout/components/newsletter-form"
```

Then, in the `Footer` component's return statement, add the following before the `div` wrapping the copyright text:

```tsx title="src/modules/layout/components/footer/index.tsx" badgeLabel="Storefront" badgeColor="blue"
<div className="border-t border-ui-border-base py-16">
  <NewsletterForm />
</div>
```

This will show the newsletter subscription form in the footer of the storefront, before the copyright text.

### Test out the Newsletter Subscription Form

You'll now test out the newsletter subscription functionality.

First, run the following command in the Medusa application's directory to start the Medusa server:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

Then, in a separate terminal, run the following command in the Next.js Starter Storefront's directory to start the Next.js server:

```bash npm2yarn
npm run dev
```

Next, open your browser at `http://localhost:8000` to view the storefront. If you scroll down to the footer, you should see the newsletter subscription form.

![Next.js Starter Storefront with the newsletter subscription form in the footer](https://res.cloudinary.com/dza7lstvk/image/upload/v1750688006/Medusa%20Resources/CleanShot_2025-06-23_at_17.12.48_2x_bjrylk.png)

Enter an email address in the form and click the "Subscribe" button. You'll see a success toast message indicating that you successfully subscribed to the newsletter.

You can verify that the subscription was successful in your Mailchimp dashboard by going to the "Audience" page. You'll see the email address you entered in the newsletter subscription form listed in your audience.

![Next.js Starter Storefront with the newsletter subscription form success message](https://res.cloudinary.com/dza7lstvk/image/upload/v1750688118/Medusa%20Resources/CleanShot_2025-06-23_at_17.15.01_2x_wetvkx.png)

If you set up the welcome email automation flow in Mailchimp, you'll receive a welcome email.

![Welcome email received from Mailchimp](https://res.cloudinary.com/dza7lstvk/image/upload/v1750688154/Medusa%20Resources/CleanShot_2025-06-23_at_12.33.09_2x_zdteda.png)

***

## Step 5: Create Automated Product Newsletter

Next, you'll schedule sending a newsletter for new products once a week. Customers subscribed to the newsletter will receive an email with the latest products added to your Medusa store.

To automate the newsletter, you will:

1. Create a workflow that retrieves the latest products and sends a notification with the product data.
2. Create a scheduled job that runs the workflow automatically once a week.

### a. Create the Workflow

A [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) is a series of actions, called steps, that complete a task with rollback and retry mechanisms. In Medusa, you build commerce features in workflows, then execute them in other customizations, such as subscribers, scheduled jobs, and API routes.

You'll create a workflow that retrieves the latest products added to your Medusa store, then creates a notification with the product data useful to send the newsletter.

Create the file `src/workflows/send-newsletter.ts` with the following content:

```ts title="src/workflows/send-newsletter.ts" highlights={workflowHighlights}
import { 
  createWorkflow, 
  when, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  sendNotificationsStep, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"

export const sendNewProductsNewsletter = createWorkflow(
  "send-new-products-newsletter", 
  (input) => {
    const { data: products } = useQueryGraphStep({
      entity: "product",
      fields: [
        "title",
        "handle",
        "thumbnail",
        "description",
      ],
      filters: {
        created_at: {
          // Get products created in the last 7 days
          // 7 days * 24 hours * 60 minutes * 60 seconds * 1000 milliseconds
          $gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
        },
      },
    })

    when({ products }, ({ products }) => products.length > 0)
      .then(() => {
        sendNotificationsStep([
          {
            to: "audience", // will be filled in by provider
            channel: "email",
            template: "new-products",
            data: {
              products,
            },
          },
        ])
      })

    return new WorkflowResponse(void 0)
  }
)
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

`createWorkflow` accepts as a second parameter a constructor function, which is the workflow's implementation.

In the workflow's constructor function, you:

1. Use the [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md) to retrieve products created in the last 7 days. You retrieve the product's title, handle, thumbnail, and description fields.
2. Use the [when-then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) utility to send the notification only if there are new products. `when` receives two parameters:
   - An object to use in the condition function.
   - A condition function that receives the first parameter object and returns a boolean indicating whether to execute the steps in the `then` block.
3. If the `when` condition is met, you use the [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) to create a notification with the following properties:
   - `to`: The audience to send the notification to. This will be filled in by the Mailchimp Notification Module Provider, so you use a placeholder value.
   - `channel`: The channel to send the notification through, which is `email` in this case.
   - `template`: The template to use for the notification, which is `new-products`.
   - `data`: An object containing the products retrieved from the query step.

Finally, you return an instance of `WorkflowResponse` indicating that the workflow was completed successfully.

You can't perform data manipulation in a workflow's constructor function. Instead, the Workflows SDK includes utility functions like `when` to perform typical operations that require accessing data values. Learn more about workflow constraints in the [Workflow Constraints](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documentation.

### b. Create a Scheduled Job

To automate executing a task at a specified interval, you can create a [scheduled job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md). A scheduled job is a background task that runs at a specified interval, such as every hour or every day.

To create a scheduled job, create the file `src/jobs/send-weekly-newsletter.ts` with the following content:

```ts title="src/jobs/send-weekly-newsletter.ts"
import {
  MedusaContainer,
} from "@medusajs/framework/types"
import { sendNewProductsNewsletter } from "../workflows/send-newsletter"

export default async function myCustomJob(container: MedusaContainer) {
  const logger = container.resolve("logger")

  logger.info("Sending weekly newsletter...")

  await sendNewProductsNewsletter(container)
    .run({
      input: {},
    })

  logger.info("Newsletter sent successfully")
}

export const config = {
  name: "send-weekly-newsletter",
  schedule: "0 0 * * 0", // Every Sunday at midnight
}
```

A scheduled job file must export:

- An asynchronous function that executes the job's logic. The function receives the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) as a parameter.
- An object with the job's configuration, including the name and the schedule. The schedule is a cron job pattern as a string.
  - You set the schedule to run the job every Sunday at midnight, which is represented by the cron pattern `0 0 * * 0`.

In the job function, you:

1. Resolve the [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) utility from the Medusa container to log messages.
2. Log a message indicating that the newsletter is being sent.
3. Execute the `sendNewProductsNewsletter` workflow by invoking it, passing the Medusa container as a parameter, then calling its `run` method.
4. Log a message indicating that the newsletter was sent successfully.

### Test Automated Newsletter

To test the automated newsletter, change the schedule of the job in `src/jobs/send-weekly-newsletter.ts` to run every minute by changing the `schedule` property in the job's configuration:

```ts title="src/jobs/send-weekly-newsletter.ts"
export const config = {
  name: "send-weekly-newsletter",
  schedule: "* * * * *", // Every minute
}
```

Also, if you didn't create any products in the past week, make sure to [create a few products in the Medusa Admin](https://docs.medusajs.com/user-guide/products/create/index.html.md).

Then, run the following command in the Medusa application's directory to start the Medusa server:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

Wait for a minute for the scheduled job to run. You should see the following logs in the terminal:

```bash
info:    Sending weekly newsletter...
info:    Newsletter sent successfully
```

This indicates that the scheduled job ran successfully.

To confirm that the newsletter was sent, check the Mailchimp dashboard by going to the "Campaigns" page. You'll find a new campaign with the title "New Products" and the date it was sent.

![Mailchimp campaigns page with the New Products campaign highlighted](https://res.cloudinary.com/dza7lstvk/image/upload/v1750689882/Medusa%20Resources/CleanShot_2025-06-23_at_17.44.27_2x_tkdtwm.png)

If you also subscribed to the newsletter, you should receive an email with the latest products added to your Medusa store.

![Mailchimp New Products newsletter email](https://res.cloudinary.com/dza7lstvk/image/upload/v1750689914/Medusa%20Resources/CleanShot_2025-06-23_at_13.29.07_2x_m5xpht.png)

Make sure to revert the job's schedule back to run every Sunday at midnight by changing the `schedule` property in the job's configuration:

```ts title="src/jobs/send-weekly-newsletter.ts"
export const config = {
  name: "send-weekly-newsletter",
  schedule: "0 0 * * 0", // Every Sunday at midnight
}
```

***

## Next Steps

You've now integrated Mailchimp with Medusa to allow customers to subscribe to a newsletter and to send automated newsletters about new products.

You can expand this feature and integration to:

- Send newsletters about other events, such as new collections or special offers. Refer to the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for the list of events you can listen to.
- Customize the registration form in the storefront to add an "opt-in for newsletter" checkbox.
- Customize the notifications to match your business's brand, or use templates defined in Mailchimp instead.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.
3. Contact the [sales team](https://medusajs.com/contact/) to get help from the Medusa team.


# Integrate Meilisearch with Medusa

In this tutorial, you'll learn how to integrate Meilisearch with Medusa to enable advanced search capabilities in your storefront.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. Medusa's architecture supports integrating third-party services, such as search engines, allowing you to build custom features around core commerce flows.

[Meilisearch](https://www.meilisearch.com/) is an open-source, fast, and relevant search engine that you can integrate with Medusa to enhance your storefront's search functionality.

## Summary

By following this tutorial, you'll learn how to:

- Install and set up Medusa.
- Integrate Meilisearch into Medusa.
- Trigger Meilisearch reindexing when a product is created, updated, or deleted, or when an admin manually triggers a reindex.
- Customize the Next.js Starter Storefront to search for products through Meilisearch.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Diagram illustrating the integration of Meilisearch with Medusa](https://res.cloudinary.com/dza7lstvk/image/upload/v1758091098/Medusa%20Resources/meilisearch-summary_bdig6e.jpg)

[Example Repository](https://github.com/medusajs/examples/tree/main/meilisearch-integration): Find the full code of the guide in this repository.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Meilisearch Module

### Prerequisites

- [Meilisearch instance (Cloud or local)](https://www.meilisearch.com/docs/learn/getting_started/cloud_quick_start)

To integrate third-party services into Medusa, you create a custom module. A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without side effects on your setup.

In this step, you'll create a custom module that provides the necessary functionalities to integrate Meilisearch with Medusa.

Refer to the [Modules documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to learn more.

Before building the module, you need to install Meilisearch's JavaScript client. Run the following command in your Medusa application's root directory:

```bash npm2yarn
npm install meilisearch
```

### Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/meilisearch`.

### Create Service

You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, which is useful if your module defines tables in the database, or connect to third-party services.

In this section, you'll create the Meilisearch Module's service and the methods necessary to manage indexed products in Meilisearch and search through them.

To create the Meilisearch Module's service, create the file `src/modules/meilisearch/service.ts` with the following content:

```ts title="src/modules/meilisearch/service.ts"
const { Meilisearch } = require("meilisearch")
import { MedusaError } from "@medusajs/framework/utils"

type MeilisearchOptions = {
  host: string;
  apiKey: string;
  productIndexName: string;
}

export type MeilisearchIndexType = "product"

export default class MeilisearchModuleService {
  private client: typeof Meilisearch
  private options: MeilisearchOptions

  constructor({}, options: MeilisearchOptions) {
    if (!options.host || !options.apiKey || !options.productIndexName) {
      throw new MedusaError(
        MedusaError.Types.INVALID_ARGUMENT, 
        "Meilisearch options are required"
      )
    }
    this.client = new Meilisearch({
      host: options.host,
      apiKey: options.apiKey,
    })
    this.options = options
  }

  // TODO: Add methods
}
```

You export a class that serves as the Meilisearch Module's service. In the service, you define two properties:

- `client`: An instance of the Meilisearch Client, which you'll use to perform actions with Meilisearch's API.
- `options`: An object of options that the module receives when it's registered, which you'll learn about later. The options contain:
  - `apiKey`: The Meilisearch API key.
  - `host`: The Meilisearch host.
  - `productIndexName`: The name of the index where products are stored.

If you want to index other types of data, such as product categories, you can add new properties for their index names in the `MeilisearchOptions` type.

A module's service receives the module's options as a second parameter in its constructor. In the constructor, you initialize the Meilisearch client using the module's options.

A module has a container that holds all resources registered in that module, and you can access those resources in the first parameter of the constructor. Learn more about it in the [Module Container documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md).

#### Index Data Method

The first method you need to add to the service is a method that receives an array of data to add or update in Meilisearch's index.

Add the following methods to the `MeilisearchModuleService` class:

```ts title="src/modules/meilisearch/service.ts"
export default class MeilisearchModuleService {
  // ...
  async getIndexName(type: MeilisearchIndexType) {
    switch (type) {
      case "product":
        return this.options.productIndexName
      default:
        throw new Error(`Invalid index type: ${type}`)
    }
  }

  async indexData(data: Record<string, unknown>[], type: MeilisearchIndexType = "product") {
    const indexName = await this.getIndexName(type)
    const index = this.client.index(indexName)
    
    // Transform data to include id as primary key for Meilisearch
    const documents = data.map((item) => ({
      ...item,
      id: item.id,
    }))

    await index.addDocuments(documents)
  }
}
```

You define two methods:

1. `getIndexName`: A method that receives an `MeilisearchIndexType` (defined in the previous snippet) and returns the index name for that type. In this case, you only have one type, `product`, so you return the product index name.
   - If you want to index other types of data, you can add more cases to the switch statement.
2. `indexData`: A method that receives an array of data and an `MeilisearchIndexType`. The method indexes the data in the Meilisearch index for the given type.

### Retrieve and Delete Methods

The next methods you'll add to the service are methods to retrieve and delete data from the Meilisearch index. You'll use these later to keep the Meilisearch index in sync with Medusa.

Add the following methods to the `MeilisearchModuleService` class:

```ts title="src/modules/meilisearch/service.ts"
export default class MeilisearchModuleService {
  // ...

  async retrieveFromIndex(documentIds: string[], type: MeilisearchIndexType = "product") {
    const indexName = await this.getIndexName(type)
    const index = this.client.index(indexName)
    
    const results = await Promise.all(
      documentIds.map(async (id) => {
        try {
          return await index.getDocument(id)
        } catch (error) {
          // Document not found, return null
          return null
        }
      })
    )

    return results.filter(Boolean)
  }

  async deleteFromIndex(documentIds: string[], type: MeilisearchIndexType = "product") {
    const indexName = await this.getIndexName(type)
    const index = this.client.index(indexName)
    
    await index.deleteDocuments(documentIds)
  }
}
```

You define two methods:

1. `retrieveFromIndex`: A method that receives an array of document IDs and a `MeilisearchIndexType`. The method retrieves the documents with the given IDs from the Meilisearch index.
2. `deleteFromIndex`: A method that receives an array of document IDs and a `MeilisearchIndexType`. The method deletes the documents with the given IDs from the Meilisearch index.

#### Search Method

The last method you'll implement is a method to search through the Meilisearch index. This method lets you expose search functionality to clients through Medusa's API routes.

Add the following method to the `MeilisearchModuleService` class:

```ts title="src/modules/meilisearch/service.ts"
export default class MeilisearchModuleService {
  // ...

  async search(query: string, type: MeilisearchIndexType = "product") {
    const indexName = await this.getIndexName(type)
    const index = this.client.index(indexName)
    
    return await index.search(query)
  }
}
```

The `search` method receives a query string and a `MeilisearchIndexType`. The method searches through the Meilisearch index for the given type, such as products, and returns the results.

### Export Module Definition

The final piece of a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/meilisearch/index.ts` with the following content:

```ts title="src/modules/meilisearch/index.ts"
import { Module } from "@medusajs/framework/utils"
import MeilisearchModuleService from "./service"

export const MEILISEARCH_MODULE = "meilisearch"

export default Module(MEILISEARCH_MODULE, {
  service: MeilisearchModuleService,
})
```

You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `meilisearch`.
2. An object with a required property `service` indicating the module's service.

You also export the module's name as `MEILISEARCH_MODULE` so you can reference it later.

### Add Module to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/meilisearch",
      options: {
        host: process.env.MEILISEARCH_HOST!,
        apiKey: process.env.MEILISEARCH_API_KEY!,
        productIndexName: process.env.MEILISEARCH_PRODUCT_INDEX_NAME!,
      },
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

You also pass an `options` property with the module's options, including the Meilisearch host, API Key, and the product index name.

### Add Environment Variables

Before you can start using the Meilisearch Module, you need to set the environment variables for the Meilisearch host, API Key, and the product index name.

Add the following environment variables to your `.env` file:

```env
MEILISEARCH_HOST=your-meilisearch-host
MEILISEARCH_API_KEY=your-meilisearch-api-key
MEILISEARCH_PRODUCT_INDEX_NAME=your-product-index-name
```

- `your-meilisearch-host` is the host of your Meilisearch instance. If you're running Meilisearch locally, it should be `http://127.0.0.1:7700`.
- `your-meilisearch-api-key` is the master key of your Meilisearch instance. If you're running Meilisearch locally, you should have set it when starting Meilisearch. If you're using Meilisearch Cloud, you can find it in the dashboard under "API Keys." Learn more in the [Meilisearch documentation](https://www.meilisearch.com/docs/learn/security/basic_security).
- `your-product-index-name` is the name of the index where you'll store products. You can choose any name you want. Even if the index doesn't exist, Meilisearch will create it when you add documents to it.

Your module is now ready for use. You'll see how to use it in the next steps.

***

## Step 3: Sync Products to Meilisearch Workflow

To keep the Meilisearch index in sync with Medusa, you need to trigger indexing when products are created, updated, or deleted in Medusa. You can also allow admins to manually trigger a reindex.

To implement the indexing functionality, you need to create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features.

Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

In this step, you'll create a workflow that indexes products in Meilisearch. In the next steps, you'll learn how to use the workflow when products are created, updated, or deleted, or when admins manually trigger a reindex.

The workflow has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve products matching specified filters and pagination parameters.
- [syncProductsStep](#syncProductsStep): Index published products in Meilisearch.
- [deleteProductsFromMeilisearchStep](#deleteProductsFromMeilisearchStep): Delete unpublished products from Meilisearch.

Medusa provides the `useQueryGraphStep` in its `@medusajs/medusa/core-flows` package. You only need to implement the other steps.

### syncProductsStep

In the second step of the workflow, you create or update indexes in Meilisearch for the products retrieved in the first step.

To create the step, create the file `src/workflows/steps/sync-products.ts` with the following content:

If you get a type error on resolving the Meilisearch Module, run the Medusa application once with the `npm run dev` or `yarn dev` command to generate the necessary type definitions, as explained in the [Automatically Generated Types guide](https://docs.medusajs.com/docs/learn/fundamentals/generated-types/index.html.md).

```ts title="src/workflows/steps/sync-products.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { MEILISEARCH_MODULE } from "../../modules/meilisearch"
import MeilisearchModuleService from "../../modules/meilisearch/service"

export type SyncProductsStepInput = {
  products: {
    id: string
    title: string
    description?: string
    handle: string
    thumbnail?: string
    categories: {
      id: string
      name: string
      handle: string
    }[]
    tags: {
      id: string
      value: string
    }[]
  }[]
}

export const syncProductsStep = createStep(
  "sync-products",
  async ({ products }: SyncProductsStepInput, { container }) => {
    const meilisearchModuleService = container.resolve<MeilisearchModuleService>(
      MEILISEARCH_MODULE
    )
    const existingProducts = await meilisearchModuleService.retrieveFromIndex(
      products.map((product) => product.id),
      "product"
    )
    const newProducts = products.filter((product) => !existingProducts.some(
      (p) => p.id === product.id)
    )
    await meilisearchModuleService.indexData(
      products as unknown as Record<string, unknown>[], 
      "product"
    )

    return new StepResponse(undefined, {
      newProducts: newProducts.map((product) => product.id),
      existingProducts,
    })
  }
  // TODO add compensation
)
```

You create a step with `createStep` from the Workflows SDK. It accepts two parameters:

1. The step's unique name, which is `sync-products`.
2. An async function that receives two parameters:
   - The step's input, which is an object holding an array of products to sync into Meilisearch.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.

In the step function, you resolve the Meilisearch Module's service from the Medusa container using the name you exported in the module definition's file.

Then, you retrieve the products that are already indexed in Meilisearch and determine which products are new. You'll learn why this is useful in a bit.

Finally, you pass the products you received in the input to Meilisearch to create or update its indices.

A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters:

1. The step's output, which in this case is `undefined`.
2. Data to pass to the step's compensation function.

#### Compensation Function

The compensation function undoes the actions performed in a step. Then, if an error occurs during the workflow's execution, the compensation functions of executed steps are called to roll back the changes. This mechanism ensures data consistency in your application, especially as you integrate external systems.

To add a compensation function to a step, pass it as a third parameter to `createStep`:

```ts title="src/workflows/steps/sync-products.ts"
export const syncProductsStep = createStep(
  // ...
  async (input, { container }) => {
    if (!input) {
      return
    }

    const meilisearchModuleService = container.resolve<MeilisearchModuleService>(
      MEILISEARCH_MODULE
    )
    
    if (input.newProducts) {
      await meilisearchModuleService.deleteFromIndex(
        input.newProducts,
        "product"
      )
    }

    if (input.existingProducts) {
      await meilisearchModuleService.indexData(
        input.existingProducts,
        "product"
      )
    }
  }
)
```

The compensation function receives two parameters:

1. The data you passed as a second parameter of `StepResponse` in the step function.
2. A context object similar to the step function that holds the Medusa container.

In the compensation function, you resolve the Meilisearch Module's service from the container. Then, you delete from Meilisearch the products that were newly indexed and revert the existing products to their original data.

#### deleteProductsFromMeilisearchStep

The `deleteProductsFromMeilisearchStep` step deletes products from Meilisearch when they are unpublished or deleted in Medusa.

Create the step at `src/workflows/steps/delete-products-from-meilisearch.ts` with the following content:

```ts title="src/workflows/steps/delete-products-from-meilisearch.ts"
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { MEILISEARCH_MODULE } from "../../modules/meilisearch"

export type DeleteProductsFromMeilisearchStep = {
  ids: string[]
}

export const deleteProductsFromMeilisearchStep = createStep(
  "delete-products-from-meilisearch-step",
  async (
    { ids }: DeleteProductsFromMeilisearchStep,
    { container }
  ) => {
    const meilisearchModuleService = container.resolve(MEILISEARCH_MODULE)
    
    const existingRecords = await meilisearchModuleService.retrieveFromIndex(
      ids, 
      "product"
    )
    await meilisearchModuleService.deleteFromIndex(
      ids,
      "product"
    )

    return new StepResponse(undefined, existingRecords)
  },
  async (existingRecords, { container }) => {
    if (!existingRecords) {
      return
    }
    const meilisearchModuleService = container.resolve(MEILISEARCH_MODULE)
    
    await meilisearchModuleService.indexData(
      existingRecords,
      "product"
    )
  }
)
```

The step receives the IDs of the products to delete as an input.

In the step, you resolve the Meilisearch Module's service and retrieve the existing records from Meilisearch. This is useful to revert the deletion if an error occurs.

Then, you delete the products from Meilisearch and pass the existing records to the compensation function.

In the compensation function, you reindex the existing records if an error occurs.

### Add Sync Products Workflow

You can now create the workflow that syncs products to Meilisearch.

To create the workflow, create the file `src/workflows/sync-products.ts` with the following content:

```ts title="src/workflows/sync-products.ts"
import { 
  createWorkflow, 
  transform, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { syncProductsStep, SyncProductsStepInput } from "./steps/sync-products"
import { deleteProductsFromMeilisearchStep } from "./steps/delete-products-from-meilisearch"

type SyncProductsWorkflowInput = {
  filters?: Record<string, unknown>
  limit?: number
  offset?: number
}

export const syncProductsWorkflow = createWorkflow(
  "sync-products",
  ({ filters, limit, offset }: SyncProductsWorkflowInput) => {
    const { data: products, metadata } = useQueryGraphStep({
      entity: "product",
      fields: [
        "id", 
        "title", 
        "description", 
        "handle", 
        "thumbnail", 
        "categories.id",
        "categories.name",
        "categories.handle",
        "tags.id",
        "tags.value",
        "status",
      ],
      pagination: {
        take: limit,
        skip: offset,
      },
      filters,
    })

    const {
      publishedProducts,
      unpublishedProductsToDelete,
    } = transform({
      products,
    }, (data) => {
      const publishedProducts: SyncProductsStepInput["products"] = []
      const unpublishedProductsToDelete: string[] = []

      data.products.forEach((product) => {
        if (product.status === "published") {
          const { status, ...rest } = product
          publishedProducts.push(rest as SyncProductsStepInput["products"][0])
        } else {
          unpublishedProductsToDelete.push(product.id)
        }
      })

      return {
        publishedProducts,
        unpublishedProductsToDelete,
      }
    })

    syncProductsStep({
      products: publishedProducts,
    })

    deleteProductsFromMeilisearchStep({
      ids: unpublishedProductsToDelete,
    })

    return new WorkflowResponse({
      products,
      metadata,
    })
  }
)
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is pagination and filter parameters for the products to retrieve.

In the workflow's constructor function, you:

1. Retrieve products from Medusa's database using `useQueryGraphStep`. This step uses Medusa's [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) tool to retrieve data across modules. You pass it the pagination and filter parameters you received in the input.
2. Use [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) to prepare two lists: one for published products to index in Meilisearch and another for unpublished products to delete from Meilisearch.
3. Index the published products in Meilisearch using `syncProductsStep`.
4. Delete unpublished products from Meilisearch using `deleteProductsFromMeilisearchStep`.

A workflow must return an instance of `WorkflowResponse`. The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is an object holding the retrieved products and their pagination details.

In the next step, you'll learn how to execute this workflow.

In workflows, you need `transform` to prepare data based on execution values. Learn more in the [Data Manipulation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) workflow documentation.

***

## Step 4: Trigger Meilisearch Sync Manually

As mentioned earlier, you'll trigger the Meilisearch sync automatically when product events occur. You also want to allow admins to manually trigger a reindex.

In this step, you'll add the functionality to trigger the `syncProductsWorkflow` manually from the Medusa Admin dashboard. This requires:

1. Creating a subscriber that listens to a custom `meilisearch.sync` event to trigger syncing products to Meilisearch.
2. Creating an API route that the Medusa Admin dashboard can call to emit the `meilisearch.sync` event, which triggers the subscriber.
3. Adding a new page or UI route to the Medusa Admin dashboard to allow admins to trigger the reindex.

### Create Products Sync Subscriber

A subscriber is an asynchronous function that listens to one or more events and performs actions when these events are emitted. A subscriber is useful when syncing data across systems, as the operation can be time-consuming and should be performed in the background.

Learn more about subscribers in the [Events and Subscribers documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).

You create a subscriber in a TypeScript or JavaScript file under the `src/subscribers` directory. So, to create the subscriber that listens to the `meilisearch.sync` event, create the file `src/subscribers/meilisearch-sync.ts` with the following content:

```ts title="src/subscribers/meilisearch-sync.ts"
import {
  SubscriberArgs,
  type SubscriberConfig,
} from "@medusajs/framework"
import { syncProductsWorkflow } from "../workflows/sync-products"

export default async function meilisearchSyncHandler({ 
  container,
}: SubscriberArgs) {
  const logger = container.resolve("logger")
  
  let hasMore = true
  let offset = 0
  const limit = 50
  let totalIndexed = 0

  logger.info("Starting product indexing...")

  while (hasMore) {
    const { result: { products, metadata } } = await syncProductsWorkflow(container)
      .run({
        input: {
          limit,
          offset,
        },
      })

    hasMore = offset + limit < (metadata?.count ?? 0)
    offset += limit
    totalIndexed += products.length
  }

  logger.info(`Successfully indexed ${totalIndexed} products`)
}

export const config: SubscriberConfig = {
  event: "meilisearch.sync",
}
```

A subscriber file must export:

1. An asynchronous function, which is the subscriber that is executed when the event is emitted.
2. A configuration object that holds the name of the event the subscriber listens to, which is `meilisearch.sync` in this case.

The subscriber function receives an object as a parameter that has a `container` property, which is the Medusa container.

In the subscriber function, you initialize variables to keep track of pagination and the total number of products indexed.

Then, you start a loop that retrieves products in batches of 50. It indexes them in Meilisearch using the `syncProductsWorkflow`. Finally, you log the total number of products indexed.

You'll learn how to emit the `meilisearch.sync` event next.

If you want to sync other data types, you can do it in this subscriber as well.

### Create API Route to Trigger Sync

To allow the Medusa Admin dashboard to trigger the `meilisearch.sync` event, you need to create an API route that emits the event.

An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts.

Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

An API route is created in a `route.ts` file under a subdirectory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

So, to create an API route at the path `/admin/meilisearch/sync`, create the file `src/api/admin/meilisearch/sync/route.ts` with the following content:

```ts title="src/api/admin/meilisearch/sync/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"

export async function POST(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const eventModuleService = req.scope.resolve(Modules.EVENT_BUS)
  await eventModuleService.emit({
    name: "meilisearch.sync",
    data: {},
  })
  res.send({
    message: "Syncing data to Meilisearch",
  })
}

```

Since you export a `POST` route handler function, you expose a `POST` API route at `/admin/meilisearch/sync`. The route handler function accepts two parameters:

1. A request object with details and context on the request, such as body parameters or authenticated user details.
2. A response object to manipulate and send the response.

In the route handler, you use the Medusa container that is available in the request object. You resolve the [Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/index.html.md). This module manages events and their subscribers.

Then, you emit the `meilisearch.sync` event using the Event Module's `emit` method. You pass it the event name.

Finally, you send a response with a message indicating that data is being synced to Meilisearch.

### Add Meilisearch Sync Page to Admin Dashboard

The last step is to add a new page to the admin dashboard. This page allows admins to trigger the reindex. You add a new page using a [UI Route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md).

A UI route is a React component that specifies the content to be shown in a new page in the Medusa Admin dashboard. You'll create a UI route to display a button that triggers the reindex when clicked.

Learn more about UI routes in the [UI Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md).

#### Configure JS SDK

Before creating the UI route, you'll configure Medusa's [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md). You can use it to send requests to the Medusa server from any client application, including your Medusa Admin customizations.

The JS SDK is installed by default in your Medusa application. To configure it, create the file `src/admin/lib/sdk.ts` with the following content:

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

You create an instance of the JS SDK using the `Medusa` class from the JS SDK. You pass it an object having the following properties:

- `baseUrl`: The base URL of the Medusa server.
- `debug`: A boolean indicating whether to log debug information into the console.
- `auth`: An object specifying the authentication type. When using the JS SDK for admin customizations, you use the `session` authentication type.

#### Create UI Route

You'll now create the UI route that displays a button to trigger the reindex. You create a UI route in a `page.tsx` file under a subdirectory of `src/admin/routes` directory. The file's path relative to `src/admin/routes` determines its path in the dashboard.

So, to create a new page under the Settings section of the Medusa Admin, create the file `src/admin/routes/settings/meilisearch/page.tsx` with the following content:

```tsx title="src/admin/routes/settings/meilisearch/page.tsx"
import { Container, Heading, Button, toast } from "@medusajs/ui"
import { useMutation } from "@tanstack/react-query"
import { sdk } from "../../../lib/sdk"
import { defineRouteConfig } from "@medusajs/admin-sdk"

const MeilisearchPage = () => {
  const { mutate, isPending } = useMutation({
    mutationFn: () => 
      sdk.client.fetch("/admin/meilisearch/sync", {
        method: "POST",
      }),
    onSuccess: () => {
      toast.success("Successfully triggered data sync to Meilisearch") 
    },
    onError: (err) => {
      console.error(err)
      toast.error("Failed to sync data to Meilisearch") 
    },
  })

  const handleSync = () => {
    mutate()
  }

  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">Meilisearch Sync</Heading>
      </div>
      <div className="px-6 py-8">
        <Button 
          variant="primary"
          onClick={handleSync}
          isLoading={isPending}
        >
          Sync Data to Meilisearch
        </Button>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Meilisearch",
})

export default MeilisearchPage
```

A UI route's file must export:

1. A React component that defines the content of the page.
2. A configuration object that specifies the route's label in the dashboard. This label is used to show a sidebar item for the new route.

In the React component, you use `useMutation` hook from `@tanstack/react-query` to create a mutation that sends a `POST` request to the API route you created earlier. In the mutation function, you use the JS SDK to send the request.

Then, in the return statement, you display a button that triggers the mutation when clicked, which sends a request to the API route you created earlier.

### Test it Out

You'll now test out the entire flow. Start by triggering the reindex manually from the Medusa Admin dashboard, then check the Meilisearch dashboard for the indexed products.

Run the following command to start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin at `http://localhost:9000/app` and log in with the credentials you set up in the first step.

Can't remember the credentials? Learn how to create a user in the [Medusa CLI reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-cli/commands/user/index.html.md).

After you log in, go to Settings from the sidebar. You'll find a new "Meilisearch" item in the Settings' sidebar. If you click on it, you'll find the page you created with the button to sync products to Meilisearch.

If you click on the button, the products will be synced to Meilisearch.

![The Meilisearch Sync page in the Medusa Admin dashboard with a button to sync products to Meilisearch](https://res.cloudinary.com/dza7lstvk/image/upload/v1758094934/Medusa%20Resources/CleanShot_2025-09-17_at_10.41.51_2x_wph4pp.png)

You can check that the sync ran and was completed by checking the Medusa logs in the terminal where you started the Medusa application. You should find the following messages:

```bash
info:    Processing meilisearch.sync which has 1 subscribers
info:    Starting product indexing...
info:    Successfully indexed 4 products
```

These messages indicate that the `meilisearch.sync` event was emitted, which triggered the subscriber you created to sync the products using the `syncProductsWorkflow`.

Finally, you can check the Meilisearch dashboard to see the indexed products. Open the Meilisearch dashboard, either Cloud or local, and choose the index you specified for products in the environment variable `MEILISEARCH_PRODUCT_INDEX_NAME`. You'll find your Medusa products indexed there.

![The Meilisearch dashboard showing the indexed products](https://res.cloudinary.com/dza7lstvk/image/upload/v1758095145/Medusa%20Resources/CleanShot_2025-09-17_at_10.45.29_2x_y8ytzf.png)

***

## Step 5: Update Index on Product Changes

You'll now automate the indexing of products whenever a change occurs. This includes when a product is created, updated, or deleted.

Similar to before, you'll create subscribers to listen to these events.

### Handle Create and Update Products

The action to perform when a product is created or updated is the same. You'll use the `syncProductsWorkflow` to sync the product to Meilisearch.

So, you only need one subscriber to handle these two events. To create the subscriber, create the file `src/subscribers/product-sync.ts` with the following content:

```ts title="src/subscribers/product-sync.ts"
import {
  SubscriberArgs,
  type SubscriberConfig,
} from "@medusajs/framework"
import { syncProductsWorkflow } from "../workflows/sync-products"

export default async function handleProductEvents({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await syncProductsWorkflow(container)
    .run({
      input: {
        filters: {
          id: data.id,
        },
      },
    })
}

export const config: SubscriberConfig = {
  event: ["product.created", "product.updated"],
}
```

The subscriber listens to the `product.created` and `product.updated` events. When either of these events is emitted, the subscriber triggers the `syncProductsWorkflow` to sync the product to Meilisearch.

When the `product.created` and `product.updated` events are emitted, the product's ID is passed in the event data payload. You can access this in the `event.data` property of the subscriber function's parameter.

So, you pass the product's ID to the `syncProductsWorkflow` as a filter to retrieve only the product that was created or updated.

#### Test it Out

To test it out, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, either create a product or update an existing one using the Medusa Admin dashboard. If you check the Meilisearch dashboard, you'll find that the product's index was created or updated.

### Handle Product Deletion

When a product is deleted, you need to remove it from the Meilisearch index. This requires a different action than creating or updating a product.

You'll create a new workflow that deletes the product from Meilisearch, then create a subscriber that listens to the `product.deleted` event to trigger the workflow.

#### Create Delete Product Workflow

Since you've already implemented the `deleteProductsFromMeilisearchStep`, you can create the workflow that uses this step.

Create the file `src/workflows/delete-products-from-meilisearch.ts` with the following content:

```ts title="src/workflows/delete-products-from-meilisearch.ts"
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
import { deleteProductsFromMeilisearchStep } from "./steps/delete-products-from-meilisearch"

type DeleteProductsFromMeilisearchWorkflowInput = {
  ids: string[]
}

export const deleteProductsFromMeilisearchWorkflow = createWorkflow(
  "delete-products-from-meilisearch",
  (input: DeleteProductsFromMeilisearchWorkflowInput) => {
    deleteProductsFromMeilisearchStep(input)
  }
)
```

The workflow receives an object with the IDs of the products to delete. It then executes the `deleteProductsFromMeilisearchStep` to delete the products from Meilisearch.

#### Create Delete Product Subscriber

Next, you'll create the subscriber that listens to the `product.deleted` event to trigger the above workflow.

Create the file `src/subscribers/product-delete.ts` with the following content:

```ts title="src/subscribers/product-delete.ts"
import {
  SubscriberArgs,
  type SubscriberConfig,
} from "@medusajs/framework"
import { deleteProductsFromMeilisearchWorkflow } from "../workflows/delete-products-from-meilisearch"

export default async function productDeleteHandler({ 
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const logger = container.resolve("logger")
  
  logger.info(`Deleting product ${data.id} from Meilisearch`)

  await deleteProductsFromMeilisearchWorkflow(container)
    .run({
      input: {
        ids: [data.id],
      },
    })
}

export const config: SubscriberConfig = {
  event: "product.deleted",
}
```

The subscriber listens to the `product.deleted` event. When the event is emitted, the subscriber triggers the `deleteProductsFromMeilisearchWorkflow`, passing it the ID of the product to delete.

#### Test it Out

To test product deletion, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, delete a product from the Medusa Admin dashboard. If you check the Meilisearch dashboard, you'll find that the product index was deleted there as well.

***

## Step 6: Search Products in Next.js Starter Storefront

The last step is to provide search functionalities to customers on your storefront. In the first step, you installed the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) along with the Medusa application.

In this step, you'll customize the Next.js Starter Storefront to add search functionality.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-search`, you can find the storefront by going back to the parent directory and changing to the `medusa-search-storefront` directory:

```bash
cd ../medusa-search-storefront # change based on your project name
```

### Install Meilisearch Packages

Before adding the implementation of search functionality, you need to install the Meilisearch packages necessary to add search functionality in your storefront.

Run the following command in the directory of your Next.js Starter Storefront:

```bash npm2yarn
npm install @meilisearch/instant-meilisearch react-instantsearch
```

This installs the Meilisearch InstantSearch JavaScript library and the React InstantSearch library. You'll use these to build the search functionality.

### Add Search Client Configuration

Next, you need to configure the Meilisearch search client.

In `src/lib/config.ts`, add the following imports at the top of the file:

```ts title="src/lib/config.ts" badgeLabel="Storefront" badgeColor="blue"
import { 
  instantMeiliSearch,
} from "@meilisearch/instant-meilisearch"
```

Then, add the following at the end of the file:

```ts title="src/lib/config.ts" badgeLabel="Storefront" badgeColor="blue"
export const { searchClient } = instantMeiliSearch(
  process.env.NEXT_PUBLIC_MEILISEARCH_HOST || "",
  process.env.NEXT_PUBLIC_MEILISEARCH_API_KEY || ""
)
```

In the code above, you create a `searchClient` object that initializes the Meilisearch client with your Meilisearch host and API Key.

### Set Environment Variables

In the storefront's `.env.local` file, add the following Meilisearch-related environment variables:

```plain badgeLabel="Storefront" badgeColor="blue"
NEXT_PUBLIC_MEILISEARCH_HOST=your_meilisearch_host
NEXT_PUBLIC_MEILISEARCH_API_KEY=your_meilisearch_api_key
NEXT_PUBLIC_MEILISEARCH_INDEX_NAME=your-products-index-name
```

Where:

- `your_meilisearch_host` is your Meilisearch host, as explained in the [Add Environment Variables section](#add-environment-variables) earlier.
- `your_meilisearch_api_key` is your Meilisearch API key with search permissions. You can retrieve it as explained in the [Meilisearch Documentation](https://www.meilisearch.com/docs/learn/security/basic_security#obtaining-api-keys).
- `your-products-index-name` is the name of the index you created in Meilisearch to store the products. You can retrieve this as explained in the [Add Environment Variables section](#add-environment-variables) earlier. You'll use this variable later.

Do not use the `masterKey` as the API key in the storefront, as it has all permissions, including write permissions. Only use an API key with search permissions.

### Add Search Modal Component

You'll now add a search modal component that customers can use to search for products. The search modal will display search results in real-time as the customer types in the search query.

Later, you'll add the search modal to the navigation bar. This allows customers to open the search modal from any page.

Create the file `src/modules/search/components/modal/index.tsx` with the following content:

```tsx title="src/modules/search/components/modal/index.tsx" badgeLabel="Storefront" badgeColor="blue"
"use client"

import React, { useEffect, useState } from "react"
import { Hits, InstantSearch, SearchBox } from "react-instantsearch"
import { searchClient } from "../../../../lib/config"
import Modal from "../../../common/components/modal"
import { Button } from "@medusajs/ui"
import Image from "next/image"
import Link from "next/link"
import { usePathname } from "next/navigation"

type Hit = {
  id: string;
  title: string;
  description: string;
  handle: string;
  thumbnail: string;
  categories: {
    id: string
    name: string
    handle: string
  }[]
  tags: {
    id: string
    value: string
  }[]
}

export default function SearchModal() {
  const [isOpen, setIsOpen] = useState(false)
  const pathname = usePathname()

  useEffect(() => {
    setIsOpen(false)
  }, [pathname])

  return (
    <>
      <div className="hidden small:flex items-center gap-x-6 h-full">
        <Button 
          onClick={() => setIsOpen(true)} 
          variant="transparent"
          className="hover:text-ui-fg-base text-small-regular px-0 hover:bg-transparent focus:!bg-transparent"
        >
          Search
        </Button>
      </div>
      <Modal isOpen={isOpen} close={() => setIsOpen(false)}>
        <InstantSearch 
          // @ts-expect-error - searchClient type issue
          searchClient={searchClient} 
          indexName={process.env.NEXT_PUBLIC_MEILISEARCH_INDEX_NAME}
        >
          <SearchBox className="w-full [&_input]:w-[94%] [&_input]:outline-none [&_button]:w-[3%]" />
          <Hits hitComponent={Hit} />
        </InstantSearch>
      </Modal>
    </>
  )
}

const Hit = ({ hit }: { hit: Hit }) => {
  return (
    <div className="flex flex-row gap-x-2 mt-4 relative" key={hit.id}>
      <Image src={hit.thumbnail} alt={hit.title} width={100} height={100} />
      <div className="flex flex-col gap-y-1">
        <h3>{hit.title}</h3>
        <p className="text-sm text-gray-500">{hit.description}</p>
      </div>
      <Link href={`/products/${hit.handle}`} className="absolute right-0 top-0 w-full h-full" aria-label={`View Product: ${hit.title}`} />
    </div>
  )
}
```

You create a `SearchModal` component that displays a search box and search results using widgets from the `react-instantsearch` library.

To display each result item (or hit), you create a `Hit` component. This component displays the product's title, description, and thumbnail. You also add a link to the product's page.

Finally, you show the search modal when the customer clicks a "Search" button. You'll add this button to the navigation bar next.

### Add Search Button to Navigation Bar

The last step is to show the search button in the navigation bar.

In `src/modules/layout/templates/nav/index.tsx`, add the following imports at the top of the file:

```tsx title="src/modules/layout/templates/nav/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import SearchModal from "@modules/search/components/modal"
```

Then, in the return statement of the `Nav` component, add the `SearchModal` component before the `div` surrounding the "Account" link:

```tsx title="src/modules/layout/templates/nav/index.tsx" badgeLabel="Storefront" badgeColor="blue"
<SearchModal />
```

The search button will now appear in the navigation bar before the Account link.

### Test it Out

To test out the storefront changes and the search API route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, start the Next.js Starter Storefront from its directory:

```bash npm2yarn
npm run dev
```

Next, go to `localhost:8000`. You'll find a Search button at the top right of the navigation bar. If you click on it, you can search through your products. You can also click on a product to view its page.

![The Next.js Starter Storefront showing the search modal with search results](https://res.cloudinary.com/dza7lstvk/image/upload/v1758096281/Medusa%20Resources/CleanShot_2025-09-17_at_11.04.26_2x_bhb9k7.png)

***

## Next Steps

You've now integrated Meilisearch with Medusa and added search functionality to your storefront. You can expand on these features to:

- Add filters to the search results. You can do that using [react-instantsearch widgets](https://www.algolia.com/doc/guides/building-search-ui/widgets/showcase/react/).
- Support indexing other data types, such as product categories. You can create subscribers and workflows for categories similar to products.

### Learn More about Medusa

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.


# Authenticate Admin Users with Okta

In this tutorial, you'll learn how to allow admin users to authenticate with Okta.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. Medusa also facilitates integrating third-party services that enrich your application with features specific to your unique business use case.

[Okta](https://www.okta.com/) is an enterprise-grade identity management service that provides secure authentication. By integrating Okta with your Medusa application, you allow users in your Okta organization to authenticate and access the Medusa Admin without needing to create separate credentials.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

[Full Code](https://github.com/medusajs/examples/tree/main/okta-integration): Find the full code of the guide in this repository.

***

## Okta Authentication Flow Summary

Before you implement the Okta integration, this section provides a high-level overview of how the Okta authentication flow works in Medusa.

![Diagram of the authentication flow between the Medusa Admin, Medusa server, and Okta](https://res.cloudinary.com/dza7lstvk/image/upload/v1764663019/Medusa%20Resources/okta-auth-overview_ngbx80.jpg)

The authentication flow consists of the following steps:

1. An admin user clicks the "Login with Okta" button on the Medusa Admin login page. This action triggers a request to the Medusa server to initiate the authentication process.
2. The Medusa server generates an authorization URL with the necessary parameters.
3. Medusa Admin redirects the user to the Okta authorization URL.
4. The user authenticates with Okta using their Okta credentials.
5. After successful authentication, Okta redirects the user back to the Medusa Admin with an authorization code.
6. The Medusa Admin validates the callback by sending a request to the Medusa server with the authorization code.
7. The Medusa server exchanges the authorization code for tokens and retrieves the user's information from Okta.
8. The Medusa server creates or updates the user's auth identity in Medusa and returns the authentication response to the Medusa Admin.
9. If this is a new user, the Medusa Admin sends a request to create a new admin user in Medusa.
10. The Medusa Admin refreshes the user's token and logs in the user.

Refer to the [Auth Route Flows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md) guide to learn more about the authentication flows in Medusa.

Medusa's [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md) provides the interface to authenticate users. It delegates the actual authentication logic to the underlying Auth Module Provider, which in this case is Okta.

So, to support the above flow, you'll create:

1. An Okta Auth Module Provider that implements the logic to authenticate users with Okta and validate the callback.
2. An admin user creation flow in the Medusa Admin that creates a new admin user in Medusa after successful authentication with Okta.
3. An admin widget that adds the "Login with Okta" button to the Medusa Admin login page and handles the authentication flow from the frontend.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

First, you'll be asked for the project's name. Then, when prompted about installing the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Okta Auth Module Provider

In this step, you'll integrate Okta as an Auth Module Provider. Later, you'll allow admin users to authenticate with Okta.

Learn more about modules in the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation.

### a. Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/okta`.

### b. Create Service

A module has a service that contains its logic. For Auth Module Providers, the service implements the logic to authenticate users.

To create the service for the Okta Auth Module Provider, create the file `src/modules/okta/service.ts` with the following content:

```ts title="src/modules/okta/service.ts" highlights={serviceHighlights}
import { AbstractAuthModuleProvider } from "@medusajs/framework/utils"
import {
  Logger,
} from "@medusajs/framework/types"

type InjectedDependencies = {
  logger: Logger
}

type Options = {
  oktaDomain: string
  clientId: string
  clientSecret: string
  redirectUri: string
}

class OktaAuthProviderService extends AbstractAuthModuleProvider {
  static DISPLAY_NAME = "Okta"
  static identifier = "okta"

  // Scopes requested from Okta during authentication
  private static readonly SCOPES = ["openid", "profile", "email"]

  protected logger_: Logger
  protected options_: Options

  constructor(
    { logger }: InjectedDependencies,
    options: Options
  ) {
    // @ts-ignore
    super(...arguments)

    this.logger_ = logger
    this.options_ = options
  }

  // TODO add methods
}

export default OktaAuthProviderService
```

An Auth Module Provider's service must extend the `AbstractAuthModuleProvider` class. You'll implement its abstract methods in a bit.

#### Static Properties

The service must also define the following static properties:

1. `DISPLAY_NAME`: The display name of the Auth Module Provider.
2. `identifier`: The unique identifier of the Auth Module Provider. This identifier is used to form the ID of the Auth Module Provider when registering it in Medusa.

#### Constructor

The constructor receives the following parameters:

1. Dependencies resolved from the [module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) that contains Framework resources available to the module.
2. Options passed to the Auth Module Provider. You expect the following options:
   - `oktaDomain`: The Okta domain of your organization.
   - `clientId`: The Client ID of your Okta application.
   - `clientSecret`: The Client Secret of your Okta application.
   - `redirectUri`: The Redirect URI of your Okta application.

### c. Implement Service Methods

In this section, you'll implement the methods required by the `AbstractAuthModuleProvider` class. You can refer to the [How to Create an Auth Module Provider](https://docs.medusajs.com/references/auth/provider/index.html.md) guide for more details about these methods.

#### validateOptions

The [validateOptions](https://docs.medusajs.com/references/auth/provider#validateOptions/index.html.md) method ensures that the module received the required options. Otherwise, it throws an error.

Add the `validateOptions` method to the `OktaAuthProviderService` class:

```ts title="src/modules/okta/service.ts"
import { MedusaError } from "@medusajs/framework/utils"

class OktaAuthProviderService extends AbstractAuthModuleProvider {
  // ...
  static validateOptions(options: Record<any, any>): void | never {
    if (!options.oktaDomain) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Okta auth provider requires oktaDomain option"
      )
    }

    if (!options.clientId) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Okta auth provider requires clientId option"
      )
    }

    if (!options.clientSecret) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Okta auth provider requires clientSecret option"
      )
    }

    if (!options.redirectUri) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Okta auth provider requires redirectUri option"
      )
    }
  }
}
```

The method receives the options passed to the module as a parameter.

It throws an error if any of the options are missing.

#### generateState

The `generateState` method is not required by the `AbstractAuthModuleProvider` class, but it's necessary to generate a unique state parameter that Okta requires during authentication.

Add the `generateState` method to the `OktaAuthProviderService` class:

```ts title="src/modules/okta/service.ts"
class OktaAuthProviderService extends AbstractAuthModuleProvider {
  // ...
  private generateState(): string {
    return (
      Math.random().toString(36).substring(2, 15) +
      Math.random().toString(36).substring(2, 15)
    )
  }
}
```

In the method, you generate a random string that will be used as the state parameter during authentication. You'll use this method in other methods to generate the state parameter.

#### authenticate

The [authenticate](https://docs.medusajs.com/references/auth/provider#authenticate/index.html.md) method is called when a user tries to authenticate with the Okta Auth Module Provider. It returns the URL to redirect the user to Okta for authentication.

Add the `authenticate` method to the `OktaAuthProviderService` class:

```ts title="src/modules/okta/service.ts"
import {
  AuthIdentityProviderService,
  AuthenticationInput,
  AuthenticationResponse,
} from "@medusajs/framework/types"

class OktaAuthProviderService extends AbstractAuthModuleProvider {
  // ...
  async authenticate(
    data: AuthenticationInput,
    authIdentityProviderService: AuthIdentityProviderService
  ): Promise<AuthenticationResponse> {
    const { body } = data

    // If callback_url is provided, use it; 
    // otherwise use the default redirectUri
    const callbackUrl = body?.callback_url || this.options_.redirectUri

    // Generate state parameter for CSRF protection
    const state = this.generateState()

    await authIdentityProviderService.setState(state, {
      callback_url: callbackUrl,
    })
    
    const params = new URLSearchParams({
      client_id: this.options_.clientId,
      response_type: "code",
      scope: OktaAuthProviderService.SCOPES.join(" "),
      redirect_uri: callbackUrl,
      state: state,
    })

    const authUrl = `${this.options_.oktaDomain}/oauth2/v1/authorize?${
      params.toString()
    }`

    // Return the authorization URL for the frontend to redirect to
    return {
      success: true,
      location: authUrl,
    }
  }
}
```

The method receives the following parameters:

1. `data`: The input data for the authentication request, which includes the request body, headers, and other useful information.
2. `authIdentityProviderService`: The service to manage [auth identities](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-identity-and-actor-types/index.html.md).

Refer to the [Auth Module Provider guide](https://docs.medusajs.com/references/auth/provider#authenticate/index.html.md) for a detailed breakdown of the parameters.

In the method, you:

1. Extract the `callback_url` from the input data. If it's not provided, you use the `redirectUri` option passed to the module.
2. Generate a state query parameter using the `generateState` method.
3. Store the state parameter along with the callback URL in the cache using the `setState` method of the `authIdentityProviderService`. This is useful for validating the state parameter later during the callback.
4. Create a URL with the required parameters to redirect the user to Okta for authentication.
5. Return the authorization URL in the response.

#### validateCallback

The [validateCallback](https://docs.medusajs.com/references/auth/provider#validateCallback/index.html.md) method is called when Okta redirects the user back to your application after authentication. It validates that the user authenticated successfully, creates the user's [auth identity](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-identity-and-actor-types/index.html.md), and returns the authentication response.

Add the `validateCallback` method to the `OktaAuthProviderService` class:

```ts title="src/modules/okta/service.ts"
class OktaAuthProviderService extends AbstractAuthModuleProvider {
  // ...
  async validateCallback(
    data: AuthenticationInput,
    authIdentityProviderService: AuthIdentityProviderService
  ): Promise<AuthenticationResponse> {
    const { query } = data

    const code = query?.code as string
    const stateKey = query?.state as string

    if (!code) {
      return {
        success: false,
        error: "Authorization code is missing",
      }
    }

    const state = await authIdentityProviderService.getState(stateKey)

    if (!state) {
      return {
        success: false,
        error: "No state provided, or session expired",
      }
    }

    const callbackUrl = state.callback_url as string

    try {
      // Exchange the authorization code for tokens
      const tokenUrl = `${this.options_.oktaDomain}/oauth2/v1/token`
      const params = new URLSearchParams({
        grant_type: "authorization_code",
        code: code,
        redirect_uri: callbackUrl,
        client_id: this.options_.clientId,
        client_secret: this.options_.clientSecret,
      })

      const tokenResponse = await fetch(tokenUrl, {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
          Accept: "application/json",
        },
        body: params.toString(),
      })

      if (!tokenResponse.ok) {
        const errorText = await tokenResponse.text()
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          `Failed to exchange code for tokens: ${errorText}`
        )
      }

      const tokenData = await tokenResponse.json()
      const accessToken = tokenData.access_token as string
      const refreshToken = tokenData.refresh_token as string | undefined
      const idToken = tokenData.id_token as string | undefined
      const expiresIn = tokenData.expires_in as number

      // Get user info from Okta using the access token
      const userInfoUrl = `${this.options_.oktaDomain}/oauth2/v1/userinfo`
      const userInfoResponse = await fetch(userInfoUrl, {
        method: "GET",
        headers: {
          Authorization: `Bearer ${accessToken}`,
          Accept: "application/json",
        },
      })

      if (!userInfoResponse.ok) {
        const errorText = await userInfoResponse.text()
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          `Failed to get user info: ${errorText}`
        )
      }

      const userInfo = await userInfoResponse.json()

      // Extract user identifier (email or sub)
      const entityId = userInfo.email || userInfo.sub

      if (!entityId) {
        return {
          success: false,
          error: "Unable to retrieve user identifier from Okta",
        }
      }

      // TODO create or update auth identity
    } catch (error) {
      this.logger_.error("Okta authentication error:", error)
      return {
        success: false,
        error: error.message || "Failed to authenticate with Okta",
      }
    }
  }
}
```

The method receives the following parameters:

1. `data`: The input data for the authentication request, which includes the request body, headers, and other useful information.
2. `authIdentityProviderService`: The service to manage [auth identities](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-identity-and-actor-types/index.html.md).

Refer to the [Auth Module Provider guide](https://docs.medusajs.com/references/auth/provider#validateCallback/index.html.md) for a detailed breakdown of the parameters.

In the method, so far, you:

1. Extract the authorization code and state parameter from the input data.
2. Retrieve the stored state using the `getState` method of the `authIdentityProviderService`. If the state is not found, return an error response.
3. Extract the `callback_url` from the retrieved state.
4. Exchange the authorization code for tokens by making a `POST` request to the Okta token endpoint.
5. If the token exchange is successful, you retrieve the token details from the response, including the access token, refresh token, ID token, and expiration time.
6. Use the access token to fetch the user's information from Okta.

Next, you'll create or update the user's auth identity in Medusa.

Replace the `// TODO create or update auth identity` comment with the following:

```ts title="src/modules/okta/service.ts"
let authIdentity
try {
  // Try to retrieve by entity_id
  authIdentity = await authIdentityProviderService.retrieve({
    entity_id: entityId,
  })

  // Update existing auth identity with latest user metadata
  authIdentity = await authIdentityProviderService.update(entityId, {
    user_metadata: {
      email: userInfo.email,
      name: userInfo.name,
      given_name: userInfo.given_name,
      family_name: userInfo.family_name,
      picture: userInfo.picture,
      updated_at: new Date().toISOString(),
    },
    provider_metadata: {
      okta_sub: userInfo.sub,
      access_token: accessToken,
      refresh_token: refreshToken,
      id_token: idToken,
      expires_at: Date.now() + expiresIn * 1000,
    },
  })
} catch (error) {
  if (error.type === MedusaError.Types.NOT_FOUND) {
    // Auth identity doesn't exist, create it
    authIdentity = await authIdentityProviderService.create({
      entity_id: entityId,
      provider_metadata: {
        okta_sub: userInfo.sub,
        access_token: accessToken,
        refresh_token: refreshToken,
        id_token: idToken,
        expires_at: Date.now() + expiresIn * 1000,
      },
      user_metadata: {
        email: userInfo.email,
        name: userInfo.name,
        given_name: userInfo.given_name,
        family_name: userInfo.family_name,
        picture: userInfo.picture,
      },
    })
  } else {
    // Re-throw if it's not a NOT_FOUND error
    throw error
  }
}

return {
  success: true,
  authIdentity,
}
```

In this part of the method, you:

1. Try to retrieve the user's auth identity using the `entity_id`.
   - If the user already has an auth identity, it means the user was previously authenticated. You update the existing auth identity with the latest user metadata and provider metadata.
   - Otherwise, you create a new auth identity with the user metadata and provider metadata.
2. Return a successful response with the auth identity.

### d. Export Module Definition

You've now finished implementing the necessary methods for the Okta Auth Module Provider.

The final piece to a module is its definition, which you export in an `index.ts` file at the module's root directory. This definition tells Medusa the module's details, including its service.

To create the module's definition, create the file `src/modules/okta/index.ts` with the following content:

```ts title="src/modules/okta/index.ts"
import OktaAuthProviderService from "./service"
import { ModuleProvider, Modules } from "@medusajs/framework/utils"

export default ModuleProvider(Modules.AUTH, {
  services: [OktaAuthProviderService],
})
```

You use `ModuleProvider` from the Modules SDK to create the module provider's definition. It accepts two parameters:

1. The name of the module that this provider belongs to, which is `Modules.AUTH` in this case.
2. An object with a required property `services` indicating the Module Provider's services.

### e. Add Module Provider to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property to the configurations:

```ts title="medusa-config.ts"
import { 
  Modules, 
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

module.exports = defineConfig({
  // ...
  modules: [
    // ...
    {
      resolve: "@medusajs/medusa/auth",
      dependencies: [
        Modules.CACHE,
        ContainerRegistrationKeys.LOGGER,
      ],
      options: {
        providers: [
          // Default email/password provider
          {
            resolve: "@medusajs/medusa/auth-emailpass",
            id: "emailpass",
          },
          // other providers...
          // Okta auth provider
          {
            resolve: "./src/modules/okta",
            id: "okta",
            options: {
              oktaDomain: process.env.OKTA_DOMAIN!,
              clientId: process.env.OKTA_CLIENT_ID!,
              clientSecret: process.env.OKTA_CLIENT_SECRET!,
              redirectUri: process.env.OKTA_REDIRECT_URI!,
            },
          },
        ],
      },
    },
  ],
})
```

To pass a Module Provider to the Auth Module, you add the `modules` property to the Medusa configuration and pass the Auth Module in its value.

The Auth Module accepts a `providers` option, which is an array of Auth Module Providers to register.

To register the Okta Auth Module Provider, you add an object to the `providers` array with the following properties:

- `resolve`: The NPM package or path to the module provider. In this case, it's the path to the `src/modules/okta` directory.
- `id`: The ID of the module provider. The Auth Module Provider is then registered with the ID `au_{identifier}_{id}`, where:
  - `{identifier}`: The identifier static property defined in the Module Provider's service, which is `okta` in this case.
  - `{id}`: The ID set in this configuration, which is also `okta` in this case.
- `options`: The options to pass to the module provider. These are the options you defined in the `Options` type of the module provider's service.

### f. Set Up Environment Variables

Next, you'll set up the environment variables whose values you passed to the Okta Auth Module Provider.

### Prerequisites

- [Okta Account. You can create a free developer account as well.](https://developer.okta.com/signup/)

#### Create Okta Application

To authenticate users with Okta, you need to create an Okta application in your Okta organization.

To create an Okta application:

1. [Log in to your Okta organization](https://developer.okta.com/login/).
2. Go to Applications -> Applications.
3. Click on "Create App Integration."
4. In the pop up, select "OIDC - OpenID Connect" as the Sign-in method and "Web Application" as the Application type. Click "Next."
5. In the creation form, enter the following details:
   - **App integration name**: A name for your application. For example, "Medusa".
   - **Sign-in redirect URIs**: The redirect URI where Okta will redirect users after authentication. For example, set it to `http://localhost:9000/app/login`. You can replace the localhost URL with your Medusa Admin's URL if it's different.
   - **Sign-out redirect URIs**: The redirect URI where Okta will redirect users after signing out. For example, set it to `http://localhost:9000/app/login`.
   - **Controlled access**: Optionally, choose "Allow everyone in your organization to access" to allow all users in your Okta organization to authenticate. You can alternatively restrict access to specific groups.
6. Click "Save" to create the application.

![Okta application creation form](https://res.cloudinary.com/dza7lstvk/image/upload/v1764672257/Medusa%20Resources/CleanShot_2025-12-02_at_12.44.00_2x_nygahc.png)

After creating the application, you'll be redirected to the application's settings page. Copy the client ID and client secret from this page, as you'll need them for the environment variables.

![Okta application settings page](https://res.cloudinary.com/dza7lstvk/image/upload/v1764672504/Medusa%20Resources/CleanShot_2025-12-02_at_12.48.03_2x_imaqs4.png)

#### Set Environment Variables

Next, set the following environment variables in your Medusa application's `.env` file:

```shell title=".env"
OKTA_DOMAIN=https://integrator...
OKTA_CLIENT_ID=your_okta_client_id
OKTA_CLIENT_SECRET=your_okta_client_secret
OKTA_REDIRECT_URI=http://localhost:9000/app/login
```

Where:

- `OKTA_DOMAIN`: The Okta domain of your organization. You can find it by going to Security -> API -> Authorization Servers in your Okta dashboard. It's the URL before `/oauth2/default`.

![Okta domain in Authorization Servers page](https://res.cloudinary.com/dza7lstvk/image/upload/v1764672662/Medusa%20Resources/CleanShot_2025-12-02_at_12.50.40_2x_exdpgj.png)

- `OKTA_CLIENT_ID`: The Client ID of your Okta application.
- `OKTA_CLIENT_SECRET`: The Client Secret of your Okta application.
- `OKTA_REDIRECT_URI`: The URL where Okta will redirect users after authentication. It's the same URL you set in the application's Sign-in redirect URIs.

The Okta integration is now ready. You'll test it out once you set up the authentication flow in the Medusa Admin.

***

## Step 3: Create Admin User API Route

In this step, you'll create an API route that creates an admin user for a newly authenticated Okta user. This also requires creating a workflow that the API route executes to create the user.

This API route is secured by Okta authentication since only authenticated users in your Okta organization can access it. However, for other authentication providers, proceed with caution if the authentication provider allows public sign-ups, such as social login with Google. In those scenarios, anyone with a Google account could create an admin user in your Medusa application.

### a. Create User Workflow

A [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) is a series of actions, called steps, that complete a task with rollback and retry mechanisms. In Medusa, you build commerce features in workflows, then execute them in other customizations, such as subscribers, scheduled jobs, and API routes.

The workflow to create a user has the following steps:

- [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md): Create an admin user in Medusa.
- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md): Associate the auth identity with the newly created user.

Medusa provides the workflow and step out-of-the-box, so you can create the workflow.

Create the file `src/workflows/create-user.ts` with the following content:

```ts title="src/workflows/create-user.ts"
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { createUsersWorkflow, setAuthAppMetadataStep } from "@medusajs/medusa/core-flows"

type WorkflowInput = {
  email: string
  auth_identity_id: string
  first_name?: string
  last_name?: string
}

export const createUserWorkflow = createWorkflow(
  "create-user",
  (input: WorkflowInput) => {
    const users = createUsersWorkflow.runAsStep({
      input: {
        users: [
          {
            email: input.email,
            first_name: input.first_name,
            last_name: input.last_name,
          },
        ],
      },
    })

    const authUserInput = transform({ input, users }, ({ input, users }) => {
      const createdUser = users[0]

      return {
        authIdentityId: input.auth_identity_id,
        actorType: "user",
        value: createdUser.id,
      }
    })

    setAuthAppMetadataStep(authUserInput)

    return new WorkflowResponse({
      user: users[0],
    })
  }
)
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

`createWorkflow` accepts as a second parameter a constructor function, which is the user's details with the ID of the auth identity that Okta created during authentication.

In the workflow's constructor function, you:

1. Create the user using the [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md).
2. Prepare the input for the `setAuthAppMetadataStep` step using [transform](https://docs.medusajs.com/docs//learn/fundamentals/workflows/variable-manipulation/index.html.md). You update the auth identity to associate it with the created user.
3. Associate the created user with the auth identity (created by Okta) using the [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md).

Finally, you return an instance of `WorkflowResponse` with the created user.

A workflow has some constraints that require you to define and manipulate variables using `transform`. Refer to the [Data Manipulation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) guide to learn more.

### b. Create Admin User API Route

Next, you'll create the API route that executes the `createUserWorkflow` to create an admin user for a newly authenticated Okta user.

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

So, create the file `src/api/okta/users/route.ts` with the following content:

```ts title="src/api/okta/users/route.ts"
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework"
import { z } from "zod"
import { createUserWorkflow } from "../../../workflows/create-user"

export const CreateUserSchema = z.object({
  email: z.string(),
  first_name: z.string().optional(),
  last_name: z.string().optional(),
})

type CreateUserBody = z.infer<typeof CreateUserSchema>

export const POST = async (
  req: AuthenticatedMedusaRequest<CreateUserBody>, 
  res: MedusaResponse
) => {
  const user = await createUserWorkflow(req.scope)
    .run({
      input: {
        email: req.validatedBody.email,
        auth_identity_id: req.auth_context!.auth_identity_id!,
        first_name: req.validatedBody.first_name,
        last_name: req.validatedBody.last_name,
      },
    })

  return res.status(200).json({ user })
}
```

You export a [Zod](https://zod.dev/) schema that you'll use to validate incoming requests to the API route.

You also export a `POST` handler function which exposes a `POST` API route at `/okta/users`.

In the API route, you execute the `createUserWorkflow` by invoking it, passing it the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), then calling its `run` method with the workflow's input.

Finally, you return the created user in the response.

### c. Apply Authentication and Validation Middlewares

Next, you'll apply middlewares to the API route to ensure that only authenticated users can access it and that the incoming requests are valid.

To apply middlewares to an API route, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import { authenticate, defineMiddlewares, validateAndTransformBody } from "@medusajs/framework/http"
import { CreateUserSchema } from "./okta/users/route"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/okta/users",
      methods: ["POST"],
      middlewares: [
        authenticate("user", "bearer", {
          allowUnregistered: true,
        }),
        validateAndTransformBody(CreateUserSchema),
        // TODO add Okta validation middleware
      ],
    },
  ],
})
```

You use `defineMiddlewares` to apply middlewares to API routes. You apply two middlewares to the `/okta/users` route:

1. [authenticate](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md): This middleware ensures only admin users with an authentication token can access the route. You set the `allowUnregistered` option to `true` to allow users who don't have an associated Medusa user yet (new Okta users) to access the route.
2. [validateAndTransformBody](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/validation/index.html.md): This middleware validates and transforms the request body using the `CreateUserSchema` schema you defined earlier.

You'll add another middleware to validate that the user was authenticated with Okta in the next section.

### d. Add Okta Validation Middleware

To ensure that only authenticated Okta users can access the user creation API route, you'll add a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) that validates that the user was authenticated with Okta.

To create the middleware, create the file `src/api/middlewares/validate-okta-provider.ts` with the following content:

```ts title="src/api/middlewares/validate-okta-provider.ts"
import { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
  MedusaNextFunction,
} from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"

export default async function validateOktaProvider(
  req: AuthenticatedMedusaRequest, 
  res: MedusaResponse, 
  next: MedusaNextFunction
) {
  if (req.auth_context.actor_id) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "User already exists"
    )
  }

  const query = req.scope.resolve("query")
  const { data: [authIdentity] } = await query.graph({
    entity: "auth_identity",
    fields: [
      "provider_identities.provider",
    ],
    filters: {
      id: req.auth_context!.auth_identity_id!,
    },
  }, {
    throwIfKeyNotFound: true,
  })

  const isOkta = authIdentity.provider_identities.some((identity) => identity?.provider === "okta")

  if (!isOkta) {
    throw new MedusaError(
      MedusaError.Types.UNAUTHORIZED,
      "Invalid provider"
    )
  }

  next()
}
```

You create a middleware function that checks if:

- The user already has an associated Medusa user. If so, you throw an error since the user shouldn't access the route.
- The user's auth identity was authenticated by Okta. You use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve the user's auth identity with its provider identities, then check whether Okta is one of the providers. If not, you throw an unauthorized error.

If both checks pass, you call the `next` function to proceed to the next middleware or route handler.

Finally, in `src/api/middlewares.ts`, add the following import at the top of the file:

```ts title="src/api/middlewares.ts"
import validateOktaProvider from "./middlewares/validate-okta-provider"
```

Then, replace the `// TODO add Okta validation middleware` comment with the new middleware:

```ts title="src/api/middlewares.ts" highlights={[["8"]]}
export default defineMiddlewares({
  routes: [
    {
      matcher: "/okta/users",
      methods: ["POST"],
      middlewares: [
        // ...
        validateOktaProvider,
      ],
    },
  ],
})
```

The `validateOktaProvider` middleware will now run before the API route handler, ensuring only authenticated Okta users can create an admin user.

You'll test out this API route after you implement the Okta authentication flow in the Medusa Admin.

***

## Step 4: Add Login with Okta in Medusa Admin

In this step, you'll customize the Medusa Admin login form to add a "Login with Okta" button that initiates the Okta authentication flow.

The Medusa Admin is customizable, allowing you to either inject custom components into existing pages or create new pages.

You'll inject a custom component into the existing login page to add the "Login with Okta" button.

While you can inject custom components into the login page, you can't remove the existing email/password login form. Therefore, both login methods will be available on the login page.

### a. Set Admin Authentication Type

By default, the Medusa Admin uses session-based authentication. However, to support third-party authentication providers like Okta, you need to switch to token-based authentication.

To set the admin authentication type, add the following environment variable to your Medusa application's `.env` file:

```shell title=".env"
ADMIN_AUTH_TYPE=jwt
```

This sets the admin authentication type to JWT (JSON Web Token), which supports third-party authentication providers.

### b. Configure JS SDK

Next, you'll configure Medusa's [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md). It allows you to send requests to the Medusa server from any client application, including your Medusa Admin customizations.

The JS SDK is installed by default in your Medusa application. To configure it, create the file `src/admin/lib/sdk.ts` with the following content:

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "jwt",
  },
})
```

You create an instance of the JS SDK using the `Medusa` class from the JS SDK. You pass it an object having the following properties:

- `baseUrl`: The base URL of the Medusa server.
- `debug`: A boolean indicating whether to log debug information into the console.
- `auth`: An object specifying the authentication type. When using the JS SDK for admin customizations, you use the `jwt` authentication type.

### c. Add Okta Icon Component

Next, you'll create a React component that renders the Okta logo. You'll use this component in the "Login with Okta" button.

Create the file `src/admin/components/okta-icon.tsx` with the following content:

```tsx title="src/admin/components/okta-icon.tsx"
export default function OktaIcon() {
  return (
    <svg 
      width="15" 
      height="15" 
      viewBox="0 0 15 15" 
      fill="none" 
      xmlns="http://www.w3.org/2000/svg"
    >
      <g clipPath="url(#clip0_16091_86347)">
        <path 
          fillRule="evenodd" 
          clipRule="evenodd" 
          d="M8.23809 0.0952381L7.92857 3.90476C7.78571 3.88095 7.64286 3.88095 7.47619 3.88095C7.28571 3.88095 7.09524 3.90476 6.92857 3.92857L6.7619 2.09524C6.7619 2.04762 6.80952 1.97619 6.85714 1.97619H7.16667L7.02381 0.119048C7.02381 0.0714286 7.07143 0 7.11905 0H8.14286C8.21429 0 8.2619 0.047619 8.23809 0.0952381ZM5.66667 0.285714C5.64286 0.238095 5.59524 0.190476 5.54762 0.214286L4.59524 0.571429C4.52381 0.595238 4.5 0.666667 4.52381 0.714286L5.30952 2.40476L5.02381 2.52381C4.97619 2.54762 4.95238 2.59524 4.97619 2.66667L5.7619 4.33333C6.04762 4.16667 6.35714 4.04762 6.69048 3.97619L5.66667 0.285714ZM3.33333 1.35714L5.54762 4.47619C5.2619 4.66667 5.02381 4.88095 4.80952 5.11905L3.45238 3.80952C3.40476 3.7619 3.40476 3.69048 3.45238 3.66667L3.69048 3.47619L2.38095 2.14286C2.33333 2.09524 2.33333 2.02381 2.38095 2L3.16667 1.35714C3.21429 1.28571 3.28571 1.30952 3.33333 1.35714ZM1.47619 3.14286C1.42857 3.11905 1.35714 3.11905 1.33333 3.16667L0.833333 4.04762C0.809524 4.09524 0.833333 4.16667 0.880952 4.19048L2.57143 5L2.40476 5.2619C2.38095 5.30952 2.40476 5.38095 2.45238 5.40476L4.14286 6.16667C4.2619 5.85714 4.42857 5.57143 4.61905 5.30952L1.47619 3.14286ZM0.214286 5.54762C0.214286 5.5 0.285714 5.45238 0.333333 5.47619L4.02381 6.42857C3.92857 6.7381 3.88095 7.07143 3.85714 7.40476L2 7.2619C1.95238 7.2619 1.90476 7.21429 1.90476 7.14286L1.95238 6.83333L0.142857 6.66667C0.0952381 6.66667 0.047619 6.61905 0.047619 6.54762L0.214286 5.54762ZM0.0952381 8.04762C0.0238095 8.04762 0 8.09524 0 8.16667L0.190476 9.16667C0.190476 9.21429 0.261905 9.2619 0.309524 9.2381L2.11905 8.7619L2.16667 9.07143C2.16667 9.11905 2.2381 9.16667 2.28571 9.14286L4.07143 8.64286C3.97619 8.33333 3.90476 8 3.88095 7.66667L0.0952381 8.04762ZM0.690476 10.6905C0.666667 10.6429 0.690476 10.5714 0.738095 10.5476L4.19048 8.90476C4.30952 9.21429 4.5 9.5 4.71429 9.7619L3.21429 10.8333C3.16667 10.8571 3.09524 10.8571 3.07143 10.8095L2.85714 10.5476L1.30952 11.619C1.2619 11.6429 1.19048 11.6429 1.16667 11.5952L0.690476 10.6905ZM4.85714 9.97619L2.16667 12.6905C2.11905 12.7381 2.11905 12.8095 2.16667 12.8333L2.95238 13.4762C3 13.5238 3.07143 13.5 3.09524 13.4524L4.19048 11.9286L4.42857 12.1429C4.47619 12.1905 4.54762 12.1667 4.57143 12.119L5.61905 10.5952C5.33333 10.4286 5.07143 10.2143 4.85714 9.97619ZM4.33333 14.3095C4.28571 14.2857 4.2619 14.2381 4.28571 14.1667L5.85714 10.7143C6.14286 10.8571 6.47619 10.9762 6.78571 11.0476L6.30952 12.8333C6.28571 12.881 6.2381 12.9286 6.19048 12.9048L5.90476 12.7857L5.40476 14.5952C5.38095 14.6429 5.33333 14.6905 5.28571 14.6667L4.33333 14.3095ZM7.04762 11.0952L6.7381 14.9048C6.7381 14.9524 6.78571 15.0238 6.83333 15.0238H7.85714C7.90476 15.0238 7.95238 14.9762 7.95238 14.9048L7.80952 13.0476H8.11905C8.16667 13.0476 8.21429 13 8.21429 12.9286L8.04762 11.0952C7.85714 11.119 7.69048 11.1429 7.5 11.1429C7.35714 11.119 7.19048 11.119 7.04762 11.0952ZM10.7381 0.809524C10.7619 0.761905 10.7381 0.690476 10.6905 0.666667L9.7381 0.309524C9.69048 0.285714 9.61905 0.333333 9.61905 0.380952L9.11905 2.19048L8.83333 2.07143C8.78571 2.04762 8.71429 2.09524 8.71429 2.14286L8.23809 3.92857C8.57143 4 8.88095 4.11905 9.16667 4.2619L10.7381 0.809524ZM12.8333 2.30952L10.1429 5.02381C9.92857 4.78571 9.66667 4.57143 9.38095 4.40476L10.4286 2.88095C10.4524 2.83333 10.5238 2.83333 10.5714 2.85714L10.8095 3.07143L11.9048 1.54762C11.9286 1.5 12 1.5 12.0476 1.52381L12.8333 2.16667C12.8571 2.21429 12.8571 2.28571 12.8333 2.30952ZM14.2619 4.45238C14.3095 4.42857 14.3333 4.35714 14.3095 4.30952L13.8095 3.42857C13.7857 3.38095 13.7143 3.35714 13.6667 3.40476L12.119 4.47619L11.9524 4.21429C11.9286 4.16667 11.8571 4.14286 11.8095 4.19048L10.3095 5.2381C10.5238 5.5 10.6905 5.78571 10.8333 6.09524L14.2619 4.45238ZM14.8095 5.83333L14.9762 6.83333C14.9762 6.88095 14.9524 6.95238 14.881 6.95238L11.0952 7.30952C11.0714 6.97619 11 6.64286 10.9048 6.33333L12.6905 5.83333C12.7381 5.80952 12.8095 5.85714 12.8095 5.90476L12.8571 6.21429L14.6667 5.7381C14.7381 5.7381 14.7857 5.78571 14.8095 5.83333ZM14.6429 9.52381C14.6905 9.54762 14.7619 9.5 14.7619 9.45238L14.9286 8.45238C14.9286 8.40476 14.9048 8.33333 14.8333 8.33333L12.9762 8.16667L13.0238 7.85714C13.0238 7.80952 13 7.7381 12.9286 7.7381L11.0714 7.59524C11.0714 7.92857 11 8.2619 10.9048 8.57143L14.6429 9.52381ZM13.6667 11.8095C13.6429 11.8571 13.5714 11.881 13.5238 11.8333L10.381 9.66667C10.5714 9.40476 10.7381 9.11905 10.8571 8.80952L12.5476 9.57143C12.5952 9.59524 12.619 9.66667 12.5952 9.71429L12.4286 10L14.119 10.8095C14.1667 10.8333 14.1905 10.9048 14.1667 10.9524L13.6667 11.8095ZM9.45238 10.5238L11.6667 13.6429C11.6905 13.6905 11.7619 13.6905 11.8095 13.6667L12.5952 13.0238C12.6429 12.9762 12.6429 12.9286 12.5952 12.881L11.2857 11.5476L11.5238 11.3571C11.5714 11.3095 11.5714 11.2619 11.5238 11.2143L10.2143 9.90476C10 10.1429 9.7381 10.3571 9.45238 10.5238ZM9.45238 14.7619C9.40476 14.7857 9.33333 14.7381 9.33333 14.6905L8.33333 11.0238C8.66667 10.9524 8.97619 10.8333 9.26191 10.6667L10.0476 12.3333C10.0714 12.381 10.0476 12.4524 10 12.4762L9.71429 12.5952L10.5 14.2857C10.5238 14.3333 10.5 14.4048 10.4524 14.4286L9.45238 14.7619Z"
          fill="currentColor"
        />
      </g>
      <defs>
      <clipPath id="clip0_16091_86347">
        <rect width="15" height="15" fill="white"/>
      </clipPath>
      </defs>
    </svg>
  )
}
```

### d. Create Login with Okta Widget

A [widget](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md) is a React component that you can inject into predefined zones in the Medusa Admin.

You'll create a widget that renders the "Login with Okta" button on the login page.

To create the widget, create the file `src/admin/widgets/okta-login.tsx` with the following content:

```tsx title="src/admin/widgets/okta-login.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Button, toast } from "@medusajs/ui"
import { decodeToken } from "react-jwt"
import { useSearchParams, useNavigate } from "react-router-dom"
import { useMutation } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"
import { useEffect, useMemo } from "react"
import OktaIcon from "../components/okta-icon"

const OKTA_AUTH_PROVIDER = "okta"

const LoginWithOkta = () => {
  const [searchParams] = useSearchParams()
  const navigate = useNavigate()

  // TODO define auth functions
}

export const config = defineWidgetConfig({
  zone: "login.after",
})

export default LoginWithOkta
```

A widget file must export:

1. A React component as the default export. This component renders the widget's UI.
2. A widget configuration object created using `defineWidgetConfig`. This configuration specifies the zone where the widget will be injected, which is `login.after` in this case.

So far, in the component, you:

1. Retrieve the URL search parameters. You'll use this to get the authorization code returned by Okta after authentication.
2. Get the `navigate` function which is necessary for navigation after successful login.

#### Initiate Okta Authentication

Next, you'll start adding the functions necessary for the authentication flow. The first function is the button click handler that initiates the Okta authentication flow.

Replace the `// TODO define auth functions` comment with the following:

```tsx title="src/admin/widgets/okta-login.tsx"
const oktaLogin = async () => {
  const result = await sdk.auth.login("user", OKTA_AUTH_PROVIDER, {})

  if (typeof result === "object" && result.location) {
    // Redirect to okta for authentication
    window.location.href = result.location
    return
  }
  
  if (typeof result !== "string") {
    // Result failed, show an error
    toast.error("Authentication failed")
    return
  }

  // User is authenticated
  navigate("/orders")
}

// TODO send callback function
```

The `oktaLogin` function uses the JS SDK's `auth.login` method to initiate the Okta authentication flow. This method sends a request to Medusa's `/auth/user/okta` API route, which executes the `authenticate` method of the Okta Auth Module Provider's service.

If the result contains a `location` property, the user is redirected to that location, which is the Okta authentication page.

If the result is not a string (the authentication token), you show an error toast notification.

Otherwise, the user is authenticated and navigated to the orders page.

#### Handle Okta Callback in Server

Next, you'll add a function that validates the Okta authentication callback in the Medusa server.

Replace the `// TODO send callback function` comment with the following:

```tsx title="src/admin/widgets/okta-login.tsx"
const sendCallback = async () => {
  try {
    return await sdk.auth.callback(
      "user", 
      OKTA_AUTH_PROVIDER, 
      Object.fromEntries(searchParams)
    )
  } catch (error) {
    toast.error("Authentication failed")
    throw error
  }
}

// TODO validate callback
```

The `sendCallback` function uses the JS SDK's `auth.callback` method to send the query parameters returned by Okta to Medusa's `/auth/user/okta/callback` API route.

This API route executes the `validateCallback` method of the Okta Auth Module Provider's service to validate the authentication.

#### Validate Callback and Create User

Next, you'll add a function that validates the callback and creates an admin user if the Okta user is new.

Replace the `// TODO validate callback` comment with the following:

```tsx title="src/admin/widgets/okta-login.tsx"
const validateCallback = async () => {
  const token = await sendCallback()

  const decodedToken = decodeToken(token) as { actor_id: string, user_metadata: Record<string, unknown> }

  const userExists = decodedToken.actor_id !== ""

  if (!userExists) {
    // Create user
    await sdk.client.fetch("/okta/users", {
      method: "POST",
      body: {
        email: decodedToken.user_metadata?.email as string,
        first_name: decodedToken.user_metadata?.given_name as string,
        last_name: decodedToken.user_metadata?.family_name as string,
      },
    })

    const newToken = await sdk.auth.refresh()

    if (!newToken) {
      toast.error("Authentication failed")
      return
    }
  }

  // User is authenticated
  navigate("/orders")
}

const { mutateAsync, isPending } = useMutation({
  mutationFn: async () => {
    if (isPending) {
      return
    }
    return await validateCallback()
  },
  onError: (error) => {
    console.error("Custom authentication error:", error)
  },
})

// TODO useEffect to trigger callback validation
```

In the `validateCallback` function, you:

1. Call the `sendCallback` function to validate the Okta authentication in the Medusa server.
2. Decode the returned authentication token to access the user's details.
3. If the `actor_id` in the decoded token is an empty string, it means the user is new. So, you:
   - Send a `POST` request to the `/okta/users` API route you created earlier to create an admin user for the new Okta user.
   - Refresh the authentication token using the JS SDK's `auth.refresh` method to get a token associated with the newly created user.
4. Finally, the user is authenticated, and you navigate them to the orders page.

You also add a mutation using [Tanstack Query](https://tanstack.com/query/latest) to handle the asynchronous operation of validating the callback.

#### Trigger Callback Validation

Next, you'll trigger the callback validation when the search parameters contain an authorization code from Okta.

Replace the `// TODO useEffect to trigger callback validation` comment with the following:

```tsx title="src/admin/widgets/okta-login.tsx"
useEffect(() => {
  // Check for provider-specific query parameters
  if (searchParams.get("code")) {
    mutateAsync()
  }
}, [searchParams, mutateAsync])

// TODO return statement
```

The `useEffect` hook runs whenever the search parameters change. If the `code` parameter is present, it calls the `mutateAsync` function to validate the callback.

#### Render Login with Okta Button

Finally, you'll render the "Login with Okta" button in the widget component. You'll also show a loading pop-up when the authentication is in progress.

Replace the `// TODO return statement` comment with the following:

```tsx title="src/admin/widgets/okta-login.tsx"
const showLoading = useMemo(() => {
  return isPending || !!searchParams.get("code")
}, [isPending, searchParams])

return (
  <>
    {showLoading && (
      <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
        <div className="flex flex-col items-center gap-4 rounded-lg bg-ui-bg-base p-8 shadow-lg">
          <div className="h-8 w-8 animate-spin rounded-full border-4 border-ui-border-base border-t-ui-fg-interactive" />
          <p className="text-ui-fg-subtle text-sm">Please wait...</p>
        </div>
      </div>
    )}
    <hr className="bg-ui-border-base my-4" />
    <Button 
      variant="secondary" 
      onClick={oktaLogin} 
      className="w-full"
      isLoading={showLoading}
    >
      <OktaIcon />
      Login with Okta
    </Button>
  </>
)
```

You define a memoized value `showLoading` that determines whether to show the loading pop-up.

Then, you render a loading pop-up if `showLoading` is `true`, and a "Login with Okta" button. The button displays the `OktaIcon` component and triggers the `oktaLogin` function when clicked.

***

## Test Okta Authentication Flow

You can now test out the Okta integration and authentication flow you implemented.

First, run the following command to start the Medusa server:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin in your browser at `http://localhost:9000/app`. You'll see a "Login with Okta" button on the login page.

![Medusa Admin login page with a "Login with Okta" button](https://res.cloudinary.com/dza7lstvk/image/upload/v1764676152/Medusa%20Resources/CleanShot_2025-12-02_at_13.48.58_2x_qjpsbk.png)

Click the button to initiate the Okta authentication flow. If you're already logged in to Okta, you'll be redirected back to the Medusa Admin immediately where you'll be logged in. If not, you'll be prompted to log in to Okta first, then redirected back to the Medusa Admin.

Once the authentication is successful, you can access the Medusa Admin dashboard as an authenticated user.

***

## Next Steps

You've now set up Okta as an authentication provider for admin users in your Medusa application. This allows your Okta organization's users to log in to the Medusa Admin using their Okta credentials without needing to manage separate credentials for Medusa.

You can also manage user access and permissions in Okta. For example, if you deny a user's access to the Medusa application in Okta, that user will no longer be able to log in to the Medusa Admin.

### Learn More about Medusa

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.


# Integrate Payload CMS with Medusa

In this tutorial, you'll learn how to integrate [Payload](https://payloadcms.com/) with Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. Medusa also facilitates integrating third-party services that enrich your application with features specific to your unique business use case.

By integrating Payload, you can manage your products' content with powerful content management capabilities, such as managing custom fields, media, localization, and more.

This guide was built with Payload v3.54.0. If you're using a different version and you run into issues, consider [opening an issue](https://github.com/medusajs/medusa/issues/new?template=docs.yml).

## Summary

By following this tutorial, you'll learn how t"o:

- Install and set up Medusa.
- Set up Payload in the Next.js Starter Storefront.
- Integrate Payload with Medusa to sync product data.
  - You'll sync product data when triggered manually by admin users, or as a result of product events in Medusa.
- Display product data from Payload in the Next.js Starter Storefront.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Diagram showcasing a flowchart of interactions between customer, admin, Medusa, and Payload](https://res.cloudinary.com/dza7lstvk/image/upload/v1754551568/Medusa%20Resources/payload-summary_ndeiw0.jpg)

[Full Code](https://github.com/medusajs/examples/tree/main/payload-integration): Find the full code of the guide in this repository.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

First, you'll be asked for the project's name. Then, when prompted about installing the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Set Up Payload in the Next.js Starter Storefront

In this step, you'll set up Payload in the Next.js Starter Storefront. This requires installing the necessary dependencies, configuring Payload, and creating collections for products and other types.

### a. Install Dependencies

In the directory of the Next.js Starter Storefront, run the following command to install the necessary dependencies:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm install payload @payloadcms/next @payloadcms/richtext-lexical sharp @payloadcms/db-postgres graphql
```

### b. Add Resolution for `undici`

Payload uses the `undici` package, but some versions of it cause an error in the Payload CLI.

To avoid these errors, add the following resolution and override to the `package.json` file of the Next.js Starter Storefront:

```json title="package.json" badgeLabel="Storefront" badgeColor="blue"
{
  "resolutions": {
    // other resolutions...
    "undici": "5.20.0"
  },
  "overrides": {
    // other overrides...
    "undici": "5.20.0"
  }
}
```

Then, re-install the dependencies to ensure the correct version of `undici` is used:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm install
```

### c. Copy Payload Template Files

Next, you'll need to copy the Payload template files into the Next.js Starter Storefront. These files allow you to access the Payload admin from the Next.js Starter Storefront.

You can find the files in the [examples GitHub repository](https://github.com/medusajs/examples/tree/main/payload-integration/storefront/src/app/\(payload\)). Copy these files into a new `src/app/(payload)` directory in the Next.js Starter Storefront.

Then, move all previous files that were under the `src/app` directory into a new `src/app/(storefront)` directory. This will ensure that the Payload admin is accessible at the `/admin` route, and the storefront is still accessible at the root route.

So, the `src/app` directory should now only include the `(payload)` and `(storefront)` directories, each containing their respective files.

![Overview of the Next.js Starter Storefront directory structure of the src directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1754474683/Medusa%20Resources/payload-storefront-dir_lt7yyw.jpg)

### d. Modify Next.js Middleware

The Next.js Starter Storefront uses a middleware to prefix all route paths with the first region's country code. While this is useful for storefront routes, it's unnecessary for the Payload admin routes.

So, you'll modify the middleware to exclude the `/admin` routes.

In `src/middleware.ts`, change the `config` object to include `/admin` in the `matcher` regex pattern:

```ts title="src/middleware.ts" badgeLabel="Storefront" badgeColor="blue"
export const config = {
  matcher: [
    "/((?!api|_next/static|_next/image|favicon.ico|images|assets|png|svg|jpg|jpeg|gif|webp|admin).*)",
  ],
}
```

### e. Add Payload Configuration

Next, you'll add the necessary configuration to run Payload in the Next.js Starter Storefront.

Create the file `src/payload.config.ts` with the following content:

```ts title="src/payload.config.ts" badgeLabel="Storefront" badgeColor="blue"
import sharp from "sharp"
import { lexicalEditor } from "@payloadcms/richtext-lexical"
import { postgresAdapter } from "@payloadcms/db-postgres"
import { buildConfig } from "payload"

export default buildConfig({
  editor: lexicalEditor(),
  collections: [
    // TODO add collections
  ],

  secret: process.env.PAYLOAD_SECRET || "",
  db: postgresAdapter({
    pool: {
      connectionString: process.env.PAYLOAD_DATABASE_URL || "",
    },
  }),
  sharp,
})
```

The configurations are mostly default Payload configurations. You configure Payload to use PostgreSQL as the database adapter. Later, you'll add collections for products and other types.

Refer to the [Payload documentation](https://payloadcms.com/docs/configuration/overview) for more information on configuring Payload.

In the configurations, you use two environment variables. To set them, add the following in your storefront's `.env.local` file:

```shell title=".env.local" badgeLabel="Storefront" badgeColor="blue"
PAYLOAD_DATABASE_URL=postgres://postgres:@localhost:5432/payload
PAYLOAD_SECRET=supersecret
```

Where:

- `PAYLOAD_DATABASE_URL` is the connection string to the PostgreSQL database that Payload will use. You don't need to create the database beforehand, as Payload will create it automatically.
- `PAYLOAD_SECRET` is your Payload secret. In production, you should use a complex and secure string.

You also need to add a path alias to the `payload.config.ts` file, as Payload will try to import it using `@payload-config`.

In `tsconfig.json`, add the following path alias:

```json title="tsconfig.json" badgeLabel="Storefront" badgeColor="blue" highlights={[["6"]]}
{
  "compilerOptions": {
    // other options...
    "paths": {
      // other paths...
      "@payload-config": ["./payload.config.ts"]
    }
  }
}
```

The `baseUrl` in the `tsconfig.json` file is set to `"./src"`, so the path alias will resolve to `src/payload.config.ts`.

### f. Customize Next.js Configurations

You also need to customize the Next.js configurations to ensure that Payload works correctly with the Next.js Starter Storefront.

In `next.config.js`, add the following `require` statement at the top of the file:

```js title="next.config.js" badgeLabel="Storefront" badgeColor="blue"
const { withPayload } = require("@payloadcms/next/withPayload")
```

Then, find the `module.exports` statement and replace it with the following:

```js title="next.config.js" badgeLabel="Storefront" badgeColor="blue"
module.exports = withPayload(nextConfig)
```

You wrap the Next.js configuration with the `withPayload` function to ensure that Payload works correctly with Next.js.

### g. Add Collections to Payload

Now that Payload is set up in your storefront, you'll create the following [collections](https://payloadcms.com/docs/configuration/collections):

- `User`: A Payload user with API key authentication, allowing you later to sync product data from Medusa to Payload.
- `Media`: A collection for media files, allowing you to manage product images and other media.
- `Product`: A collection for products, which will be synced with Medusa's product data.

Once you're done, you'll add the collections to `src/payload.config.ts`.

#### User Collection

To create the `User` collection, create the file `src/collections/Users.ts` with the following content:

```ts title="src/collections/Users.ts" badgeLabel="Storefront" badgeColor="blue"
import type { CollectionConfig } from "payload"

export const Users: CollectionConfig = {
  slug: "users",
  admin: {
    useAsTitle: "email",
  },
  auth: {
    useAPIKey: true,
  },
  fields: [],
}
```

The `Users` collection allows you to manage users that can log into the Payload admin with email and API key authentication.

Refer to the [Payload documentation](https://payloadcms.com/docs/authentication/api-keys) to learn more about API key authentication.

#### Media Collection

To create the `Media` collection, create the file `src/collections/Media.ts` with the following content:

```ts title="src/collections/Media.ts" badgeLabel="Storefront" badgeColor="blue"
import { CollectionConfig } from "payload"

export const Media: CollectionConfig = {
  slug: "media",
  upload: {
    staticDir: "public",
    imageSizes: [
      {
        name: "thumbnail",
        width: 400,
        height: 300,
        position: "centre",
      },
      {
        name: "card",
        width: 768,
        height: 1024,
        position: "centre",
      },
      {
        name: "tablet",
        width: 1024,
        height: undefined,
        position: "centre",
      },
    ],
    adminThumbnail: "thumbnail",
    mimeTypes: ["image/*"],
    pasteURL: {
      allowList: [
        {
          protocol: "http",
          hostname: "localhost",
        },
        {
          protocol: "https",
          hostname: "medusa-public-images.s3.eu-west-1.amazonaws.com",
        },
        {
          protocol: "https",
          hostname: "medusa-server-testing.s3.amazonaws.com",
        },
        {
          protocol: "https",
          hostname: "medusa-server-testing.s3.us-east-1.amazonaws.com",
        },
      ],
    },
  },
  fields: [
    {
      name: "alt",
      type: "text",
      label: "Alt Text",
      required: false,
    },
  ],
}
```

The `Media` collection will store media files, such as product images. You can upload files to the [Storage Adapters](https://payloadcms.com/docs/upload/storage-adapters) configured in Payload, such as AWS S3 or local storage. The above configurations point to the `public` directory of the Next.js Starter Storefront as the upload directory.

Note that you allow pasting URLs from specific sources, such as the Medusa public images S3 bucket. This allows you to paste Medusa's stock image URLs in the Payload admin.

#### Product Collection

Finally, you'll add the `Product` collection, which will be synced with Medusa's product data.

Create the file `src/collections/Products.ts` with the following content:

```ts title="src/collections/Products.ts" badgeLabel="Storefront" badgeColor="blue" highlights={productCollectionHighlights}
import { CollectionConfig } from "payload"

export const Products: CollectionConfig = {
  slug: "products",
  admin: {
    useAsTitle: "title",
  },
  fields: [
    {
      name: "medusa_id",
      type: "text",
      label: "Medusa Product ID",
      required: true,
      unique: true,
      admin: {
        description: "The unique identifier from Medusa",
        hidden: true, // Hide this field in the admin UI
      },
      access: {
        update: ({ req }) => !!req.query.is_from_medusa,
      },
    },
    {
      name: "title",
      type: "text",
      label: "Title",
      required: true,
      admin: {
        description: "The product title",
      },
    },
    {
      name: "handle",
      type: "text",
      label: "Handle",
      required: true,
      admin: {
        description: "URL-friendly unique identifier",
      },
      validate: (value: any) => {
        // validate URL-friendly handle
        if (typeof value !== "string") {
          return "Handle must be a string"
        }
        if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {
          return "Handle must be URL-friendly (lowercase letters, numbers, and hyphens only)"
        }
        return true
      },
    },
    {
      name: "subtitle",
      type: "text",
      label: "Subtitle",
      required: false,
      admin: {
        description: "Product subtitle",
      },
    },
    {
      name: "description",
      type: "richText",
      label: "Description",
      required: false,
      admin: {
        description: "Detailed product description",
      },
    },
    {
      name: "thumbnail",
      type: "upload",
      relationTo: "media" as any,
      label: "Thumbnail",
      required: false,
      admin: {
        description: "Product thumbnail image",
      },
    },
    {
      name: "images",
      type: "array",
      label: "Product Images",
      required: false,
      fields: [
        {
          name: "image",
          type: "upload",
          relationTo: "media" as any,
          required: true,
        },
      ],
      admin: {
        description: "Gallery of product images",
      },
    },
    {
      name: "seo",
      type: "group",
      label: "SEO",
      fields: [
        {
          name: "meta_title",
          type: "text",
          label: "Meta Title",
          required: false,
        },
        {
          name: "meta_description",
          type: "textarea",
          label: "Meta Description",
          required: false,
        },
        {
          name: "meta_keywords",
          type: "text",
          label: "Meta Keywords",
          required: false,
        },
      ],
      admin: {
        description: "SEO-related fields for better search visibility",
      },
    },
    {
      name: "options",
      type: "array",
      fields: [
        {
          name: "title",
          type: "text",
          label: "Option Title",
          required: true,
        },
        {
          name: "medusa_id",
          type: "text",
          label: "Medusa Option ID",
          required: true,
          admin: {
            description: "The unique identifier for the option from Medusa",
            hidden: true, // Hide this field in the admin UI
          },
          access: {
            update: ({ req }) => !!req.query.is_from_medusa,
          },
        },
      ],
      validate: (value: any, { req, previousValue }) => {
        // TODO add validation to ensure that the number of options cannot be changed
      },
    },
    {
      name: "variants",
      type: "array",
      fields: [
        {
          name: "title",
          type: "text",
          label: "Variant Title",
          required: true,
        },
        {
          name: "medusa_id",
          type: "text",
          label: "Medusa Variant ID",
          required: true,
          admin: {
            description: "The unique identifier for the variant from Medusa",
            hidden: true, // Hide this field in the admin UI
          },
          access: {
            update: ({ req }) => !!req.query.is_from_medusa,
          },
        },
        {
          name: "option_values",
          type: "array",
          fields: [
            {
              name: "medusa_id",
              type: "text",
              label: "Medusa Option Value ID",
              required: true,
              admin: {
                description: "The unique identifier for the option value from Medusa",
                hidden: true, // Hide this field in the admin UI
              },
              access: {
                update: ({ req }) => !!req.query.is_from_medusa,
              },
            },
            {
              name: "medusa_option_id",
              type: "text",
              label: "Medusa Option ID",
              required: true,
              admin: {
                description: "The unique identifier for the option from Medusa",
                hidden: true, // Hide this field in the admin UI
              },
              access: {
                update: ({ req }) => !!req.query.is_from_medusa,
              },
            },
            {
              name: "value",
              type: "text",
              label: "Value",
              required: true,
            },
          ],
        },
      ],
      validate: (value: any, { req, previousValue }) => {
        // TODO add validation to ensure that the number of variants cannot be changed
      },
    },
  ],
  hooks: {
    // TODO add 
  },
  access: {
    create: ({ req }) => !!req.query.is_from_medusa,
    delete: ({ req }) => !!req.query.is_from_medusa,
  },
}
```

You create a `Products` collection having the following fields:

- `medusa_id`: The product's ID in Medusa, which is useful when syncing data between Payload and Medusa.
- `title`: The product's title.
- `handle`: A URL-friendly unique identifier for the product.
- `subtitle`: An optional subtitle for the product.
- `description`: A rich text description of the product.
- `thumbnail`: An optional thumbnail image for the product.
- `images`: An array of images for the product.
- `seo`: A group of fields for SEO-related information, such as meta title, description, and keywords.
- `options`: An array of product options, such as size or color.
- `variants`: An array of product variants, each with its own title and option values.

All of these fields will be filled from Medusa.

In addition, you also add the following [access-control](https://payloadcms.com/docs/access-control/overview) configurations:

- You disallow creating or deleting products from the Payload admin, as these actions should only be performed from Medusa.
- You disallow updating the `medusa_id` fields from the Payload admin, as these fields are managed by Medusa.

#### Add Validation for Options and Variants

Payload admin users can only manage the content of product options and variants, but they shouldn't be able to remove or add new options or variants.

To ensure this behavior, you'll add validation to the `options` and `variants` fields in the `Products` collection.

First, replace the `validate` function in the `options` field with the following:

```ts title="src/collections/Products.ts" badgeLabel="Storefront" badgeColor="blue"
export const Products: CollectionConfig = {
  // other configurations...
  fields: [
    // other fields...
    {
      name: "options",
      // other configurations...
      validate: (value: any, { req, previousValue }) => {
        if (req.query.is_from_medusa) {
          return true // Skip validation if the request is from Medusa
        }
        
        if (!Array.isArray(value)) {
          return "Options must be an array"
        }

        const optionsChanged = value.length !== previousValue?.length || value.some((option) => {
          return !option.medusa_id || !previousValue?.some(
            (prevOption) => (prevOption as any).medusa_id === option.medusa_id
          )
        })

        // Prevent update if the number of options is changed
        return !optionsChanged || "Options cannot be changed in number"
      },
    },
  ],
}
```

If the request is from Medusa (which is indicated by the `is_from_medusa` query parameter), the validation is skipped.

Otherwise, you only allow updating the options if the number of options remains the same and each option has a `medusa_id` that matches an existing option in the previous value.

Next, replace the `validate` function in the `variants` field with the following:

```ts title="src/collections/Products.ts" badgeLabel="Storefront" badgeColor="blue"
export const Products: CollectionConfig = {
  // other configurations...
  fields: [
    // other fields...
    {
      name: "variants",
      // other configurations...
      validate: (value: any, { req, previousValue }) => {
        if (req.query.is_from_medusa) {
          return true // Skip validation if the request is from Medusa
        }

        if (!Array.isArray(value)) {
          return "Variants must be an array"
        }

        const changedVariants = value.length !== previousValue?.length || value.some((variant: any) => {
          return !variant.medusa_id || !previousValue?.some(
            (prevVariant: any) => prevVariant.medusa_id === variant.medusa_id
          )
        })

        if (changedVariants) {
          // Prevent update if the number of variants is changed
          return "Variants cannot be changed in number"
        }
        
        const changedOptionValues = value.some((variant: any) => {
          if (!Array.isArray(variant.option_values)) {
            return true // Invalid structure
          }

          const previousVariant = previousValue?.find(
            (v: any) => v.medusa_id === variant.medusa_id
          ) as Record<string, any> | undefined

          return variant.option_values.length !== previousVariant?.option_values.length || 
            variant.option_values.some((optionValue: any) => {
              return !optionValue.medusa_id || !previousVariant?.option_values.some(
                (prevOptionValue: any) => prevOptionValue.medusa_id === optionValue.medusa_id
              )
            })
        })

        return !changedOptionValues || "Option values cannot be changed in number"
      },
    },
  ],
}
```

If the request is from Medusa, the validation is skipped.

Otherwise, the function validates that:

- The number of variants is the same as the previous value.
- Each variant has a `medusa_id` that matches an existing variant in the previous value.
- The number of option values for each variant is the same as the previous value.
- Each option value has a `medusa_id` that matches an existing option value in the previous value.

If any of these validations fail, an error message is returned, preventing the update.

#### Add Hooks to Normalize Product Data

Next, you'll add a `beforeChange` hook to the `Products` collection that will normalize incoming `description` data to [rich-text format](https://payloadcms.com/docs/rich-text/overview).

In `src/collections/Products.ts`, add the following import statement at the top of the file:

```ts title="src/collections/Products.ts" badgeLabel="Storefront" badgeColor="blue"
import { convertLexicalToMarkdown, convertMarkdownToLexical, editorConfigFactory } from "@payloadcms/richtext-lexical"
```

Then, in the `Products` collection, add a `beforeChange` property to the `hooks` configuration:

```ts title="src/collections/Products.ts" badgeLabel="Storefront" badgeColor="blue"
export const Products: CollectionConfig = {
  // other configurations...
  hooks: {
    beforeChange: [
      async ({ data, req }) => {
        if (typeof data.description === "string") {
          data.description = convertMarkdownToLexical({
            editorConfig: await editorConfigFactory.default({
              config: req.payload.config,
            }),
            markdown: data.description,
          })
        }

        return data
      },
    ],
  },
}
```

This hook checks if the `description` field is a string and converts it to rich-text format. This ensures that a description coming from Medusa is properly formatted when stored in Payload.

#### Add Collections to Payload's Configurations

Now that you've created the collections, you need to add them to Payload's configurations.

In `src/payload.config.ts`, add the following imports at the top of the file:

```ts title="src/payload.config.ts" badgeLabel="Storefront" badgeColor="blue"
import { Users } from "./collections/Users"
import { Products } from "./collections/Products"
import { Media } from "./collections/Media"
```

Then, add the collections to the `collections` array of the `buildConfig` function:

```ts title="src/payload.config.ts" badgeLabel="Storefront" badgeColor="blue"
export default buildConfig({
  // ...
  collections: [
    Users,
    Products,
    Media,
  ],
  // ...
})
```

### i. Generate Payload Imports Map

Before running the Payload admin, you need to generate the imports map that Payload uses to resolve the collections and other configurations.

Run the following command in the Next.js Starter Storefront directory:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npx payload generate:importmap
```

This command generates the `src/app/(payload)/admin/importMap.js` file that Payload needs.

### j. Run the Payload Admin

You can now run the Payload admin in the Next.js Starter Storefront and create an admin user.

To start the Next.js Starter Storefront, run the following command in the Next.js Starter Storefront directory:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

Then, open the Payload admin in your browser at `http://localhost:8000/admin`. The first time you access it, Payload will create a database at the connection URL you provided in the `.env.local` file.

Then, you'll see a form to create a new admin user. Enter the user's credentials and submit the form.

Once you're logged in, you can see the `Products`, `Users`, and `Media` collections in the Payload admin.

![Payload Admin Dashboard](https://res.cloudinary.com/dza7lstvk/image/upload/v1754477731/Medusa%20Resources/CleanShot_2025-08-06_at_13.55.06_2x_bhdkkd.png)

***

## Step 3: Integrate Payload with Medusa

Now that Payload is set up in the Next.js Starter Storefront, you'll create a Payload [Module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to integrate it with Medusa.

A module is a reusable package that provides functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules and their structure.

### a. Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/payload`.

### b. Create Types for the Module

Next, you'll create a types file that will hold the types for the module's options and service methods.

Create the file `src/modules/payload/types.ts` with the following content:

```ts title="src/modules/payload/types.ts" badgeLabel="Medusa application" badgeColor="green"
export interface PayloadModuleOptions {
  serverUrl: string;
  apiKey: string;
  userCollection?: string;
}
```

For now, the file only contains the `PayloadModuleOptions` interface, which defines the options that the module will receive. It includes:

- `serverUrl`: The URL of the Payload server.
- `apiKey`: The API key for authenticating with the Payload server.
- `userCollection`: The name of the user collection in Payload. This is optional and defaults to `users`. It's useful for the authentication header when sending requests to the Payload API.

### c. Create Service

A module has a service that contains its logic. So, the Payload Module's service will contain the logic to create, update, retrieve, and delete data in Payload.

Create the file `src/modules/payload/service.ts` with the following content:

```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green"
import {
  PayloadModuleOptions,
} from "./types"
import { MedusaError } from "@medusajs/framework/utils"

type InjectedDependencies = {
  // inject any dependencies you need here
};

export default class PayloadModuleService {
  private baseUrl: string
  private headers: Record<string, string>
  private defaultOptions: Record<string, any> = {
    is_from_medusa: true,
  }

  constructor(
    container: InjectedDependencies,
    options: PayloadModuleOptions
  ) {
    this.validateOptions(options)
    this.baseUrl = `${options.serverUrl}/api`
    
    this.headers = {
      "Content-Type": "application/json",
      "Authorization": `${
        options.userCollection || "users"
      } API-Key ${options.apiKey}`,
    }
  }

  validateOptions(options: Record<any, any>): void | never {
    if (!options.serverUrl) {
      throw new MedusaError(
        MedusaError.Types.INVALID_ARGUMENT,
        "Payload server URL is required"
      )
    }
    
    if (!options.apiKey) {
      throw new MedusaError(
        MedusaError.Types.INVALID_ARGUMENT,
        "Payload API key is required"
      )
    }
  }
}
```

The constructor of a module's service receives the following parameters:

1. The [Module container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) that allows you to resolve module and Framework resources. You don't need to resolve any resources in this module, so you can leave it empty.
2. The module options, which you'll [pass to the module when you register it later](#e-add-module-to-medusas-configurations) in the Medusa application.

In the constructor, you validate the module options and set up the Payload base URL and headers that are necessary to send requests to Payload.

### c. Add Methods to the Service

Next, you'll add methods to the service that allow you to create, update, retrieve, and delete products in Payload.

#### makeRequest Method

The `makeRequest` private method is a utility function that makes HTTP requests to the Payload API. You'll use this method in other public methods that perform operations in Payload.

Add the `makeRequest` method to the `PayloadModuleService` class:

```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green"
export default class PayloadModuleService {
  // ...
  private async makeRequest<T = any>(
    endpoint: string,
    options: RequestInit = {}
  ): Promise<T> {
    const url = `${this.baseUrl}${endpoint}`
    
    try {
      const response = await fetch(url, {
        ...options,
        headers: {
          ...this.headers,
          ...options.headers,
        },
      })

      if (!response.ok) {
        const errorData = await response.json().catch(() => ({}))
        throw new MedusaError(
          MedusaError.Types.UNEXPECTED_STATE,
          `Payload API error: ${response.status} ${response.statusText}. ${
            errorData.message || ""
          }`
        )
      }

      return await response.json()
    } catch (error) {
      throw new MedusaError(
        MedusaError.Types.UNEXPECTED_STATE,
        `Failed to communicate with Payload: ${JSON.stringify(error)}`
      )
    }
  }
}
```

The `makeRequest` method receives the endpoint to call and the options for the request. It constructs the full URL, makes the request, and returns the response data as JSON.

If the request fails, it throws a `MedusaError` with the error message.

#### create Method

The `create` method will allow you to create an entry in a Payload collection, such as `Products`.

Before you create the method, you'll need to add necessary types for its parameters and return value.

In `src/modules/payload/types.ts`, add the following types:

```ts title="src/modules/payload/types.ts" badgeLabel="Medusa application" badgeColor="green"
export interface PayloadCollectionItem {
  id: string;
  createdAt: string;
  updatedAt: string;
  medusa_id: string;
  [key: string]: any;
}

export interface PayloadUpsertData {
  [key: string]: any;
}

export interface PayloadQueryOptions {
  depth?: number;
  locale?: string;
  fallbackLocale?: string;
  select?: string;
  populate?: string;
  limit?: number;
  page?: number;
  sort?: string;
  where?: Record<string, any>;
}

export interface PayloadItemResult<T = PayloadCollectionItem> {
  doc: T;
  message: string;
}
```

You define the following types:

- `PayloadCollectionItem`: an item in a Payload collection.
- `PayloadUpsertData`: the data required to create or update an item in a Payload collection.
- `PayloadQueryOptions`: the options for querying items in a Payload collection, which you can learn more about in the [Payload documentation](https://payloadcms.com/docs/queries/overview).
- `PayloadItemResult`: the result of a querying or performing an operation on a Payload item, which includes the item and a message.

Next, add the following import statements at the top of the `src/modules/payload/service.ts` file:

```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green"
import {
  PayloadCollectionItem,
  PayloadUpsertData,
  PayloadQueryOptions,
  PayloadItemResult,
} from "./types"
import qs from "qs"
```

You import the types you just defined and the `qs` library, which you'll use to stringify query options.

Then, add the `create` method to the `PayloadModuleService` class:

```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green"
export default class PayloadModuleService {
  // ... other methods
  async create<T extends PayloadCollectionItem = PayloadCollectionItem>(
    collection: string,
    data: PayloadUpsertData,
    options: PayloadQueryOptions = {}
  ): Promise<PayloadItemResult<T>> {

    const stringifiedQuery = qs.stringify({
      ...options,
      ...this.defaultOptions,
    }, {
      addQueryPrefix: true,
    })

    const endpoint = `/${collection}/${stringifiedQuery}`

    const result = await this.makeRequest<PayloadItemResult<T>>(endpoint, {
      method: "POST",
      body: JSON.stringify(data),
    })
    return result
  }
}
```

The `create` method receives the following parameters:

- `collection`: the slug of the collection in Payload where you want to create an item. For example, `products`.
- `data`: the data for the new item you want to create.
- `options`: optional query options for the request.

In the method, you use the `makeRequest` method to send a `POST` request to Payload, passing it the endpoint and request body data.

Finally, you return the result of the request that contains the created item and a message.

#### update Method

Next, you'll add the `update` method that allows you to update an existing item in a Payload collection.

Add the `update` method to the `PayloadModuleService` class:

```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green"
export default class PayloadModuleService {
  // ... other methods
  async update<T extends PayloadCollectionItem = PayloadCollectionItem>(
    collection: string,
    data: PayloadUpsertData,
    options: PayloadQueryOptions = {}
  ): Promise<PayloadItemResult<T>> {

    const stringifiedQuery = qs.stringify({
      ...options,
      ...this.defaultOptions,
    }, {
      addQueryPrefix: true,
    })

    const endpoint = `/${collection}/${stringifiedQuery}`

    const result = await this.makeRequest<PayloadItemResult<T>>(endpoint, {
      method: "PATCH",
      body: JSON.stringify(data),
    })

    return result
  }
}
```

Similar to the `create` method, the `update` method receives the collection slug, the data to update, and optional query options.

In the method, you use the `makeRequest` method to send a `PATCH` request to Payload, passing it the endpoint and request body data.

Finally, you return the result of the request that contains the updated item and a message.

#### delete Method

Next, you'll add the `delete` method that allows you to delete an item from a Payload collection.

First, add the following type to `src/modules/payload/types.ts`:

```ts title="src/modules/payload/types.ts" badgeLabel="Medusa application" badgeColor="green"
export interface PayloadApiResponse<T = any> {
  data?: T;
  errors?: Array<{
    message: string;
    field?: string;
  }>;
  message?: string;
}
```

This represents a generic response from Payload, which can include data, errors, and a message.

Then, add the following import statement at the top of the `src/modules/payload/service.ts` file:

```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green"
import {
  PayloadApiResponse,
} from "./types"
```

After that, add the `delete` method to the `PayloadModuleService` class:

```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green"
export default class PayloadModuleService {
  // ... other methods
  async delete(
    collection: string,
    options: PayloadQueryOptions = {}
  ): Promise<PayloadApiResponse> {

    const stringifiedQuery = qs.stringify({
      ...options,
      ...this.defaultOptions,
    }, {
      addQueryPrefix: true,
    })

    const endpoint = `/${collection}/${stringifiedQuery}`

    const result = await this.makeRequest<PayloadApiResponse>(endpoint, {
      method: "DELETE",
    })

    return result
  }
}
```

The `delete` method receives as parameters the collection slug and optional query options.

In the method, you use the `makeRequest` method to send a `DELETE` request to Payload, passing it the endpoint.

Finally, you return the result of the request that contains any data, errors, or a message.

#### find Method

The last method you'll add for now is the `find` method, which allows you to retrieve items from a Payload collection.

First, add the following type to `src/modules/payload/types.ts`:

```ts title="src/modules/payload/types.ts" badgeLabel="Medusa application" badgeColor="green"
export interface PayloadBulkResult<T = PayloadCollectionItem> {
  docs: T[];
  totalDocs: number;
  limit: number;
  page: number;
  totalPages: number;
  hasNextPage: boolean;
  hasPrevPage: boolean;
  nextPage: number | null;
  prevPage: number | null;
  pagingCounter: number;
}
```

This type represents the result of a bulk query to a Payload collection, which includes an array of documents and pagination information.

Then, add the following import statement at the top of the `src/modules/payload/service.ts` file:

```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green"
import {
  PayloadBulkResult,
} from "./types"
```

After that, add the `find` method to the `PayloadModuleService` class:

```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green"
export default class PayloadModuleService {
  async find(
    collection: string,
    options: PayloadQueryOptions = {}
  ): Promise<PayloadBulkResult<PayloadCollectionItem>> {

    const stringifiedQuery = qs.stringify({
      ...options,
      ...this.defaultOptions,
    }, {
      addQueryPrefix: true,
    })

    const endpoint = `/${collection}${stringifiedQuery}`

    const result = await this.makeRequest<
      PayloadBulkResult<PayloadCollectionItem>
    >(endpoint)

    return result
  }
}
```

The `find` method receives the collection slug and optional query options.

In the method, you use the `makeRequest` method to send a `GET` request to Payload, passing it the endpoint with the query options.

Finally, you return the result of the request that contains an array of documents and pagination information.

### d. Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at the module's root directory. This definition tells Medusa the name of the module, its service, and optionally its loaders.

To create the module's definition, create the file `src/modules/payload/index.ts` with the following content:

```ts title="src/modules/payload/index.ts" badgeLabel="Medusa application" badgeColor="green"
import { Module } from "@medusajs/framework/utils"
import PayloadModuleService from "./service"

export const PAYLOAD_MODULE = "payload"

export default Module(PAYLOAD_MODULE, {
  service: PayloadModuleService,
})
```

You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `payload`.
2. An object with a required property `service` indicating the module's service. You also pass the loader you created to ensure it's executed when the application starts.

Aside from the module definition, you export the module's name as `PAYLOAD_MODULE` so you can reference it later.

### e. Add Module to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:

```ts title="medusa-config.ts" badgeLabel="Medusa application" badgeColor="green"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/payload",
      options: {
        serverUrl: process.env.PAYLOAD_SERVER_URL || "http://localhost:8000",
        apiKey: process.env.PAYLOAD_API_KEY,
        userCollection: process.env.PAYLOAD_USER_COLLECTION || "users",
      },
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package's name.

You also pass an `options` property with the module's options. You'll set the values of these options next.

### f. Set Environment Variables

To use the Payload Module, you need to set the module options in the environment variables of your Medusa application.

One of these options is the API key of a Payload admin user. To get the API key:

1. Start the Next.js Starter Storefront with the following command:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

2. Open `localhost:8000/admin` in your browser and log in with the admin user you created earlier.
3. Click on the "Users" collection in the sidebar.
4. Choose your admin user from the list.
5. Click on the "Enable API key" checkbox and copy the API key that appears.
6. Click the "Save" button to save the changes.

![The user form with the API key enabled](https://res.cloudinary.com/dza7lstvk/image/upload/v1754479684/Medusa%20Resources/CleanShot_2025-08-06_at_14.27.25_2x_gasihl.png)

Next, add the following environment variables to your Medusa application's `.env` file:

```shell title=".env" badgeLabel="Medusa application" badgeColor="green"
PAYLOAD_SERVER_URL=http://localhost:8000
PAYLOAD_API_KEY=your_api_key_here
PAYLOAD_USER_COLLECTION=users
```

Make sure to replace `your_api_key_here` with the API key you copied from the Payload admin.

The Payload Module is now ready for use. You'll add customizations next to sync product data between Medusa and Payload.

***

## Step 4: Create Virtual Read-Only Link to Products

Medusa's [Module Links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md) feature allows you to virtually link data models from external services to modules in your Medusa application. Then, when you retrieve data from Medusa, you can also retrieve the linked data from the third-party service automatically.

In this step, you'll define a virtual read-only link between the `Products` collection in Payload and the `Product` model in Medusa. Later, you'll be able to retrieve products from Payload while retrieving products in Medusa.

### a. Define the Link

To define a virtual read-only link, create the file `src/links/product-payload.ts` with the following content:

```ts title="src/links/product-payload.ts" badgeLabel="Medusa application" badgeColor="green"
import { defineLink } from "@medusajs/framework/utils"
import ProductModule from "@medusajs/medusa/product"
import { PAYLOAD_MODULE } from "../modules/payload"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    field: "id",
  },
  {
    linkable: {
      serviceName: PAYLOAD_MODULE,
      alias: "payload_product",
      primaryKey: "product_id",
    },
  },
  {
    readOnly: true,
  }
)
```

The `defineLink` function accepts three parameters:

- An object of the first data model that is part of the link. In this case, it's the `Product` model from Medusa's Product Module.
- An object of the second data model that is part of the link. In this case, it's the `Products` collection from the Payload Module. You set the following properties:
  - `serviceName`: the name of the Payload Module, which is `payload`.
  - `alias`: an alias for the linked data model, which is `payload_product`. You'll use this alias to reference the linked data model in queries.
  - `primaryKey`: the primary key of the linked data model, which is `product_id`. Medusa will look for this field in the retrieved `Products` from payload to match it with the `id` field of the `Product` model.
- An object with the `readOnly` property set to `true`, indicating that this link is read-only. This means you can only retrieve the linked data, but you don't manage the link in the database.

### b. Add list Method to the Payload Module Service

When you retrieve products from Medusa with their `payload_product` link, Medusa will call the `list` method of the Payload Module's service to retrieve the linked products from Payload.

So, in `src/modules/payload/service.ts`, add a `list` method to the `PayloadModuleService` class:

```ts title="src/modules/payload/service.ts" badgeLabel="Medusa application" badgeColor="green"
export default class PayloadModuleService {
  // ... other methods
  async list(
    filter: {
      product_id: string | string[]
    }
  ) {
    const collection = filter.product_id ? "products" : "unknown"
    const ids = Array.isArray(filter.product_id) ? filter.product_id : [filter.product_id]
    const result = await this.find(
      collection,
      {
        where: {
          medusa_id: {
            in: ids.join(","),
          },
        },
        depth: 2,
      }
    )

    return result.docs.map((doc) => ({
      ...doc,
      product_id: doc.medusa_id,
    }))
  }
}
```

The `list` method receives a `filter` object with an `product_id` property, which is the Medusa product ID(s) to retrieve their corresponding data from Payload.

In the method, you call the `find` method of the Payload Module's service to retrieve products from the `products` collection in Payload. You pass a `where` query parameter to filter products by their `medusa_id` field.

Finally, you return an array of the payload products. You set the `product_id` field to the value of the `medusa_id` field, which is used to match the linked data in Medusa.

You can now retrieve products from Payload while retrieving products in Medusa. You'll learn how to do this in the upcoming steps.

The `list` method is implemented to be re-usable with different collections and data models. For example, if you add a `Categories` collection in Payload, you can use the same `list` method to retrieve categories by their `medusa_id` field. In that case, the `filter` object would have a `category_id` property instead of `product_id`, and you can set the `collection` variable to `"categories"`.

***

## Step 5: Create Payload Product Workflow

In this step, you'll create the functionality to create a Medusa product in Payload. You'll later execute that functionality either when triggered by an admin user, or automatically when a product is created in Medusa.

You create custom commerce features in [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task. A workflow is similar to a function, but it allows you to track its executions' progress, define roll-back logic, and configure other advanced features.

Refer to the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) to learn more.

The workflow to create a Payload product will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the product data from Medusa
- [createPayloadItemsStep](#createPayloadItemsStep): Create the product in Payload
- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md): Store the Payload product ID in Medusa

You only need to create the `createPayloadItemsStep`, as the other two steps are already available in Medusa.

### createPayloadItemsStep

The `createPayloadItemsStep` will create an item in a Payload collection, such as `Products`.

To create the step, create the file `src/workflows/steps/create-payload-items.ts` with the following content:

If you get a type error on resolving the Payload Module, run the Medusa application once with the `npm run dev` or `yarn dev` command to generate the necessary type definitions, as explained in the [Automatically Generated Types guide](https://docs.medusajs.com/docs/learn/fundamentals/generated-types/index.html.md).

```ts title="src/workflows/steps/create-payload-items.ts" badgeLabel="Medusa application" badgeColor="green" highlights={createPayloadItemsStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PayloadUpsertData } from "../../modules/payload/types"
import { PAYLOAD_MODULE } from "../../modules/payload"

type StepInput = {
  collection: string
  items: PayloadUpsertData[]
}

export const createPayloadItemsStep = createStep(
  "create-payload-items",
  async ({ items, collection }: StepInput, { container }) => {
    const payloadModuleService = container.resolve(PAYLOAD_MODULE)
    
    const createdItems = await Promise.all(
      items.map(async (item) => await payloadModuleService.create(
        collection,
        item
      ))
    )

    return new StepResponse({
      items: createdItems.map((item) => item.doc),
    }, {
      ids: createdItems.map((item) => item.doc.id),
      collection,
    })
  },
  async (data, { container }) => {
    if (!data) {
      return
    }
    const { ids, collection } = data

    const payloadModuleService = container.resolve(PAYLOAD_MODULE)

    await payloadModuleService.delete(
      collection,
      {
        where: {
          id: {
            in: ids.join(","),
          },
        },
      }
    )
  }
)
```

You create a step with the `createStep` function. It accepts three parameters:

1. The step's unique name.
2. An async function that receives two parameters:
   - The step's input, which is an object holding the collection slug and an array of items to create in Payload.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.
3. An async compensation function that undoes the actions performed by the step function. This function is only executed if an error occurs during the workflow's execution.

In the step function, you resolve the Payload Module's service from the container. Then, you use its `create` method to create the items in Payload.

A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters:

1. The step's output, which is an object that contains the created items.
2. Data to pass to the step's compensation function.

In the compensation function, you again resolve the Payload Module's service from the Medusa container, then delete the created items from Payload.

### Create Payload Product Workflow

You can now create the workflow that creates products in Payload.

To create the workflow, create the file `src/workflows/create-payload-products.ts` with the following content:

```ts title="src/workflows/create-payload-products.ts" badgeLabel="Medusa application" badgeColor="green" highlights={createPayloadProductsWorkflowHighlights}
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { createPayloadItemsStep } from "./steps/create-payload-items"
import { updateProductsWorkflow, useQueryGraphStep } from "@medusajs/medusa/core-flows"

type WorkflowInput = {
  product_ids: string[]
}

export const createPayloadProductsWorkflow = createWorkflow(
  "create-payload-products",
  (input: WorkflowInput) => {
    const { data: products } = useQueryGraphStep({
      entity: "product",
      fields: [
        "id",
        "title",
        "handle",
        "subtitle",
        "description",
        "created_at",
        "updated_at",
        "options.*",
        "variants.*",
        "variants.options.*",
        "thumbnail",
        "images.*",
      ],
      filters: {
        id: input.product_ids,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const createData = transform({
      products,
    }, (data) => {
      return {
        collection: "products",
        items: data.products.map((product) => ({
          medusa_id: product.id,
          createdAt: product.created_at as string,
          updatedAt: product.updated_at as string,
          title: product.title,
          handle: product.handle,
          subtitle: product.subtitle,
          description: product.description || "",
          options: product.options.map((option) => ({
            title: option.title,
            medusa_id: option.id,
          })),
          variants: product.variants.map((variant) => ({
            title: variant.title,
            medusa_id: variant.id,
            option_values: variant.options.map((option) => ({
              medusa_id: option.id,
              medusa_option_id: option.option?.id,
              value: option.value,
            })),
          })),
        })),
      }
    })

    const { items } = createPayloadItemsStep(
      createData
    )

    const updateData = transform({
      items,
    }, (data) => {
      return data.items.map((item) => ({
        id: item.medusa_id,
        metadata: {
          payload_id: item.id,
        },
      }))
    })

    updateProductsWorkflow.runAsStep({
      input: {
        products: updateData,
      },
    })

    return new WorkflowResponse({
      items,
    })
  }
)
```

You create a workflow using the `createWorkflow` function. It accepts the workflow's unique name as a first parameter.

It accepts a second parameter: a constructor function that holds the workflow's implementation. The function accepts an input object holding the IDs of the products to create in Payload.

In the workflow, you:

1. Retrieve the products from Medusa using the `useQueryGraphStep`.
   - This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve data across modules.
2. Prepare the data to create the products in Payload.
   - To manipulate data in a workflow, you need to use the `transform` function. Learn more in the [Data Manipulation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) documentation.
3. Create the products in Payload using the `createPayloadItemsStep` you created earlier.
4. Prepare the data to update the products in Medusa with the Payload product IDs.
   - You store the payload ID in the `metadata` field of the Medusa product.
5. Update the products in Medusa using the `updateProductsWorkflow`.

A workflow must return an instance of `WorkflowResponse` that accepts the data to return to the workflow's executor.

You'll use this workflow in the next steps to create Medusa products in Payload.

***

## Step 6: Trigger Product Creation in Payload

In this step, you'll allow Medusa Admin users to trigger the creation of Medusa products in Payload. To implement this, you'll create:

- An [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) that emits a `products.sync-payload` event.
- A [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that listens to the `products.sync-payload` event and executes the `createPayloadProductsWorkflow`.
- A [setting page](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) in the Medusa Admin that allows admin users to trigger the product creation in Payload.

### a. Trigger Product Sync API Route

An API route is a REST endpoint that exposes functionalities to clients, such as storefronts and the Medusa Admin.

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

Refer to the [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) to learn more about them.

Create the file `src/api/admin/payload/sync/[collection]/route.ts` with the following content:

```ts title="src/api/admin/payload/sync/[collection]/route.ts" badgeLabel="Medusa application" badgeColor="green"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const POST = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const { collection } = req.params
  const eventModuleService = req.scope.resolve("event_bus")

  await eventModuleService.emit({
    name: `${collection}.sync-payload`,
    data: {},
  })

  return res.status(200).json({
    message: `Syncing ${collection} with Payload`,
  })
}
```

Since you export a `POST` route handler function, you're exposing a `POST` API route at `/admin/payload/sync/[collection]`, where `[collection]` is a path parameter that represents the collection slug in Payload.

In the function, you resolve the [Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/index.html.md)'s service and emit a `{collection}.sync-payload` event, where `{collection}` is the collection slug passed in the request.

Finally, you return a success response with a message indicating that the collection is being synced with Payload.

### b. Create Subscriber for the Event

Next, you'll create a subscriber that listens to the `products.sync-payload` event and executes the `createPayloadProductsWorkflow`.

A subscriber is an asynchronous function that is executed whenever its associated event is emitted.

Refer to the [Subscribers documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) to learn more about subscribers.

To create a subscriber, create the file `src/subscribers/products-sync-payload.ts` with the following content:

```ts title="src/subscribers/products-sync-payload.ts" badgeLabel="Medusa application" badgeColor="green" highlights={productsSyncPayloadSubscriberHighlights}
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { createPayloadProductsWorkflow } from "../workflows/create-payload-products"

export default async function productSyncPayloadHandler({
  container,
}: SubscriberArgs) {
  const query = container.resolve("query")

  const limit = 1000
  let offset = 0
  let count = 0
  
  do {
    const { 
      data: products,
      metadata: { count: totalCount } = {},
    } = await query.graph({
      entity: "product",
      fields: [
        "id",
        "metadata",
      ],
      pagination: {
        take: limit,
        skip: offset,
      },
    })

    count = totalCount || 0
    offset += limit
    const filteredProducts = products.filter((product) => !product.metadata?.payload_id)

    if (filteredProducts.length === 0) {
      break
    }

    await createPayloadProductsWorkflow(container)
      .run({
        input: {
          product_ids: filteredProducts.map((product) => product.id),
        },
      })

  } while (count > offset + limit)
}

export const config: SubscriberConfig = {
  event: "products.sync-payload",
}
```

A subscriber file must export:

- An asynchronous function, which is the subscriber function that is executed when the event is emitted.
- A configuration object that defines the event the subscriber listens to.

In the subscriber, you use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve all products from Medusa.

Then, you filter the products to only include those that don't have a payload product ID set in `product.metadata.payload_id`, and you execute the `createPayloadProductsWorkflow` with the filtered products' IDs.

Whenever the `products.sync-payload` event is emitted, the subscriber will be executed, which will create the products in Payload.

### c. Create Setting Page in Medusa Admin

Next, you'll create a setting page in the Medusa Admin that allows admin users to trigger syncing products with Payload.

#### Initialize JS SDK

To send requests from your Medusa Admin customizations to the Medusa server, you need to initialize the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md).

Create the file `src/admin/lib/sdk.ts` with the following content:

```ts title="src/admin/lib/sdk.ts" badgeLabel="Medusa application" badgeColor="green"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

Refer to the [JS SDK documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) to learn more about initializing the SDK.

#### Create the Setting Page

A setting page is a [UI route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) that adds a custom page to the Medusa Admin under the Settings section. The UI route is a React component that renders the page's content.

Refer to the [UI Routes](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) documentation to learn more.

To create the setting page, create the file `src/admin/routes/settings/payload/page.tsx` with the following content:

```tsx title="src/admin/routes/settings/payload/page.tsx" badgeLabel="Medusa application" badgeColor="green"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Button, Container, Heading, toast } from "@medusajs/ui"
import { useMutation } from "@tanstack/react-query"
import { sdk } from "../../../lib/sdk"

const PayloadSettingsPage = () => {
  const { 
    mutateAsync: syncProductsToPayload,
    isPending: isSyncingProductsToPayload,
  } = useMutation({
    mutationFn: (collection: string) => 
      sdk.client.fetch(`/admin/payload/sync/${collection}`, {
        method: "POST",
      }),
    onSuccess: () => toast.success(`Triggered syncing collection data with Payload`),
  })

  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h1">Payload Settings</Heading>
      </div>
      <div className="flex flex-col gap-4 px-6 py-4">
        <p>
          This page allows you to trigger syncing your Medusa data with Payload. It
          will only create items not in Payload.
        </p>
        <Button
          variant="primary"
          onClick={() => syncProductsToPayload("products")}
          isLoading={isSyncingProductsToPayload}
        >
          Sync Products to Payload
        </Button>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Payload",
})

export default PayloadSettingsPage
```

A settings page file must export:

1. A React component that renders the page. This is the file's default export.
2. A configuration object created with the `defineRouteConfig` function. It accepts an object with properties that define the page's configuration, such as its sidebar label.

In the page's component, you define a mutation function using Tanstack Query and the JS SDK. This function will send a `POST` request to the API route you created earlier to trigger syncing products with Payload.

Then, you render a button that, when clicked, calls the mutation function to trigger the syncing process.

### d. Test Product Syncing

You can now test syncing products from Medusa to Payload. To do that:

1. Start your Medusa application with the following command:

```bash npm2yarn badgeLabel="Medusa application" badgeColor="green"
npm run dev
```

2. Run the Next.js Starter Storefront with the command:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

3. Open the Medusa Admin at `localhost:9000/app` and log in with your admin user.
4. Go to Settings -> Payload.
5. On the setting page, click the "Sync Products to Payload" button.

![The Payload Settings page with the Sync Products button](https://res.cloudinary.com/dza7lstvk/image/upload/v1754486648/Medusa%20Resources/CleanShot_2025-08-06_at_16.23.45_2x_ubug2y.png)

You'll see a success message indicating that the products are being synced with Payload. You can also confirm that the event was triggered by checking the Medusa server logs for the following message:

```bash
info:    Processing products.sync-payload which has 1 subscribers
```

To check that the products were created in Payload, open the Payload admin at `localhost:8000/admin` and go to "Products" from the sidebar. You should see your Medusa products listed there.

![The Products collection in Payload with Medusa products](https://res.cloudinary.com/dza7lstvk/image/upload/v1754486747/Medusa%20Resources/CleanShot_2025-08-06_at_16.25.31_2x_h874oa.png)

If you click on a product, you can edit its details, such as its title or description.

***

## Step 7: Automatically Create Product in Payload

In this step, you'll handle the `product.created` event to automatically create a product in Payload whenever a product is created in Medusa.

You already have the workflow to create a product in Payload, so you only need to create a subscriber that listens to the `product.created` event and executes the `createPayloadProductsWorkflow`.

Create the file `src/subscribers/product-created.ts` with the following content:

```ts title="src/subscribers/product-created.ts" badgeLabel="Medusa application" badgeColor="green"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { createPayloadProductsWorkflow } from "../workflows/create-payload-products"

export default async function productCreatedHandler({
  event: { data },
  container,
}: SubscriberArgs<{
  id: string
}>) {
  await createPayloadProductsWorkflow(container)
    .run({
      input: {
        product_ids: [data.id],
      },
    })
}

export const config: SubscriberConfig = {
  event: "product.created",
}
```

This subscriber listens to the `product.created` event and executes the `createPayloadProductsWorkflow` with the created product's ID.

### Test Automatic Product Creation

To test out the automatic product creation in Payload, make sure that both the Medusa application and the Next.js Starter Storefront are running.

Then, create a product in Medusa using the Medusa Admin. If you check the Products collection in the Payload admin, you should see the newly created product there as well.

***

## Step 8: Customize Storefront to Display Payload Products

Now that you've integrated Payload with Medusa, you can customize the Next.js Starter Storefront to display product content from Payload. By doing so, you can show product content and assets that are optimized for the storefront.

In this step, you'll customize the Next.js Starter Storefront to show the product title, description, images, and option values from Payload.

### a. Fetch Payload Data with Product Data

When you fetch product data in the Next.js Starter Storefront from the Medusa server, you can also retrieve the linked product data from Payload.

To do this, go to `src/lib/data/products.ts` in your Next.js Starter Storefront. You'll find a `listProducts` function that uses the JS SDK to fetch products from the Medusa server.

Find the `sdk.client.fetch` call and add `*payload_product` to the `fields` query parameter:

```ts title="src/lib/data/products.ts" badgeLabel="Storefront" badgeColor="blue" highlights={[["19"]]}
export const listProducts = async ({
  // ...
}: {
  //...
}): Promise<{
  // ...
}> => {
  // ...
  return sdk.client
    .fetch<{ products: HttpTypes.StoreProduct[]; count: number }>(
      `/store/products`,
      {
        method: "GET",
        query: {
          limit,
          offset,
          region_id: region?.id,
          fields:
            "*variants.calculated_price,+variants.inventory_quantity,+metadata,+tags,*payload_product",
          ...queryParams,
        },
        headers,
        next,
        cache: "force-cache",
      }
    )
  // ...
}
```

Passing this field is possible because you [defined the virtual read-only link](#step-4-create-virtual-read-only-link-to-products) between the `Product` model in Medusa and the `Products` collection in Payload.

Medusa will now return the payload data of a product from Payload and include it in the `payload_product` field of the product object.

### b. Define Payload Product Type

Next, you'll define a TypeScript type that adds the `payload_product` property to Medusa's `StoreProduct` type.

In `src/types/global.ts`, add the following imports at the top of the file:

```ts title="src/types/global.ts" badgeLabel="Storefront" badgeColor="blue"
import { StoreProduct } from "@medusajs/types"
// @ts-ignore
import type { SerializedEditorState } from "@payloadcms/richtext-lexical/lexical"
```

Then, add the following type definition at the end of the file:

```ts title="src/types/global.ts" badgeLabel="Storefront" badgeColor="blue"
export type StoreProductWithPayload = StoreProduct & {
  payload_product?: {
    medusa_id: string
    title: string
    handle: string
    subtitle?: string
    description?: SerializedEditorState
    thumbnail?: {
      id: string
      url: string
    }
    images: {
      id: string
      image: {
        id: string
        url: string
      }
    }[]
    options: {
      medusa_id: string
      title: string
    }[]
    variants: {
      medusa_id: string
      title: string
      option_values: {
        medusa_option_id: string
        value: string
      }[]
    }[]
  }
}
```

The `StoreProductWithPayload` type extends the `StoreProduct` type from Medusa and adds the `payload_product` property. This property contains the product data from Payload, including its title, description, images, options, and variants.

### c. Display Payload Product Title and Description

Next, you'll customize the product details page to display the product title and description from Payload.

To do that, you need to customize the `ProductInfo` component in `src/modules/products/templates/product-info/index.tsx`.

First, add the following import statement at the top of the file:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { StoreProductWithPayload } from "../../../../types/global"
// @ts-ignore
import { RichText } from "@payloadcms/richtext-lexical/react"
```

Then, change the type of the `product` prop to `StoreProductWithPayload`:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
type ProductInfoProps = {
  product: StoreProductWithPayload
}
```

Next, find in the `ProductInfo` component's `return` statement where the product title is displayed and replace it with the following:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"], ["6"], ["7"], ["8"], ["9"], ["10"], ["11"]]}
return (
  <div id="product-info">
    <div className="flex flex-col...">
      {/* ... */}
      <Heading
        level="h2"
        className="text-3xl leading-10 text-ui-fg-base"
        data-testid="product-title"
      >
        {product?.payload_product?.title || product.title}
      </Heading>
      {/* ... */}
    </div>
  </div>
)
```

Also, find where the product description is displayed and replace it with the following:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"], ["6"], ["7"], ["8"], ["9"], ["10"], ["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]}
return (
  <div id="product-info">
    <div className="flex flex-col...">
      {/* ... */}
      {product?.payload_product?.description !== undefined && 
        <RichText data={product.payload_product.description} />
      }
      
      {product?.payload_product?.description === undefined && (
        <Text
          className="text-medium text-ui-fg-subtle whitespace-pre-line"
          data-testid="product-description"
        >
          {product.description}
        </Text>
      )}
      {/* ... */}
    </div>
  </div>
)
```

If the product has a description in Payload, it will be displayed using Payload's `RichText` component, which renders the rich text content. Otherwise, it will display the product description from Medusa.

### d. Display Payload Product Images

Next, you'll display the product images from Payload in the product details page and in the product preview component that is shown in the product list.

#### Add Image Utility Functions

You'll first create utility functions useful for retrieving the images of a product.

Create the file `src/lib/util/payload-images.ts` with the following content:

```ts title="src/lib/util/payload-images.ts" badgeLabel="Storefront" badgeColor="blue"
import { StoreProductWithPayload } from "../../types/global"

export function getProductImages(product: StoreProductWithPayload) {
  return product?.payload_product?.images?.map((image) => ({
    id: image.id,
    url: formatPayloadImageUrl(image.image.url),
  })) || product.images || []
}

export function formatPayloadImageUrl(url: string): string {
  return url.replace(/^\/api\/media\/file/, "")
}
```

You define two functions:

- `getProductImages`: This function accepts a product and returns either the images from Payload or the images from Medusa if the product doesn't have images in Payload.
- `formatPayloadImageUrl`: This function formats the image URL from Payload by removing the `/api/media/file` prefix, which is not needed for displaying the image in the storefront.

#### Update ImageGallery Props

Next, you'll update the type of the `ImageGallery` component's props to receive an array of objects rather than an array of Medusa images. This ensures the component can accept images from Payload.

In `src/modules/products/components/image-gallery/index.tsx`, update the `ImageGalleryProps` type to the following:

```tsx title="src/modules/products/components/image-gallery/index.tsx" badgeLabel="Storefront" badgeColor="blue"
type ImageGalleryProps = {
  images: {
    id: string
    url: string
  }[]
}
```

The `ImageGallery` component can now accept an array of image objects, each with an `id` and a `url`.

#### Display Images in Product Details Page

To display the product images in the product details page, add the following imports at the top of `src/modules/products/templates/index.tsx`:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { StoreProductWithPayload } from "../../../types/global"
import { getProductImages } from "../../../lib/util/payload-images"
```

Next, change the type of the `product` prop to `StoreProductWithPayload`:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
type ProductTemplateProps = {
  product: StoreProductWithPayload
  // ...
}
```

Then, add the following before the `ProductTemplate` component's `return` statement:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["7"]]}
const ProductTemplate: React.FC<ProductTemplateProps> = ({
  product,
  region,
  countryCode,
}) => {
  // ...
  const productImages = getProductImages(product)
  // ...
}
```

You retrieve the images to display using the `getProductImages` function you created earlier.

Finally, update the `images` prop of the `ImageGallery` component in the `return` statement:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"]]}
return (
  <>
    {/* ... */}
    <ImageGallery images={productImages} />
    {/* ... */}
  </>
)
```

The images on the product's details page will now be the images from Payload if available, or the images from Medusa if not.

#### Display Images in Product Preview

To display the product images in the product preview component that is displayed in the product list, add the following imports at the top of `src/modules/products/components/product-preview/index.tsx`:

```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { StoreProductWithPayload } from "../../../../types/global"
import { formatPayloadImageUrl, getProductImages } from "../../../../lib/util/payload-images"
```

Then, change the type of the `product` prop to `StoreProductWithPayload`:

```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"]]}
export default async function ProductPreview({
  product,
  // ...
}: {
  product: StoreProductWithPayload
  // ...
}) {
  // ...
}
```

Next, add the following before the `return` statement:

```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["8"]]}
export default async function ProductPreview({
  // ...
}: {
  // ...
}) {
  // ...

  const productImages = getProductImages(product)

  // ...
}
```

You retrieve the images to display using the `getProductImages` function you created earlier.

After that, update the `thumbnail` and `images` props of the `Thumbnail` component in the `return` statement:

```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"], ["6"], ["7"], ["8"], ["9"]]}
return (
  <LocalizedClientLink href={`/products/${product.handle}`} className="group">
    {/* ... */}
    <Thumbnail
      thumbnail={product.payload_product?.thumbnail ? 
        formatPayloadImageUrl(product.payload_product.thumbnail.url) : 
        product.thumbnail
      }
      images={productImages}
      size="full"
      isFeatured={isFeatured}
    />
    {/* ... */}
  </LocalizedClientLink>
)
```

The thumbnail shown in the product listing will now use the thumbnail from Payload if available, or the thumbnail from Medusa if not.

You'll also display the product title from Payload in the product preview. Find the following lines in the `return` statement:

```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"], ["5"], ["6"]]}
return (
  <LocalizedClientLink href={`/products/${product.handle}`} className="group">
    {/* ... */}
    <Text className="text-ui-fg-subtle" data-testid="product-title">
      {product.title}
    </Text>
    {/* ... */}
  </LocalizedClientLink>
)
```

And replace them with the following:

```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"], ["5"], ["6"]]}
return (
  <LocalizedClientLink href={`/products/${product.handle}`} className="group">
    {/* ... */}
    <Text className="text-ui-fg-subtle" data-testid="product-title">
      {product.payload_product?.title || product.title}
    </Text>
    {/* ... */}
  </LocalizedClientLink>
)
```

The product title in the product preview will now be the title from Payload if available, or the title from Medusa if not.

### e. Display Product Options and Values

The last change you'll make is to display the title of product options and their values from Payload in the product details page.

In `src/modules/products/components/product-actions/index.tsx`, add the following import at the top of the file:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { StoreProductWithPayload } from "../../../../types/global"
```

Then, change the type of the `product` prop to `StoreProductWithPayload`:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
type ProductActionsProps = {
  product: StoreProductWithPayload
  // ...
}
```

Next, find the `optionsAsKeymap` function and replace it with the following:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const optionsAsKeymap = (
  variantOptions: HttpTypes.StoreProductVariant["options"],
  payloadData: StoreProductWithPayload["payload_product"]
) => {
  const firstVariant = payloadData?.variants?.[0]
  return variantOptions?.reduce((acc: Record<string, string>, varopt: any) => {
    acc[varopt.option_id] = firstVariant?.option_values.find(
      (v) => v.medusa_option_id === varopt.id
    )?.value || varopt.value
    return acc
  }, {})
}
```

You update the function to receive a `payloadData` parameter, which is the product data from Payload. This allows you to retrieve the option values from Payload instead of Medusa.

Then, in the `ProductActions` component, update all usages of the `optionsAsKeymap` function to pass the `product.payload_product` data:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"]]}
// update all usages of optionsAsKeymap
const variantOptions = optionsAsKeymap(
  product.variants[0].options,
  product.payload_product
)
```

Finally, in the `return` statement, find the loop over `product.options` and replace it with the following:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={productActionsComponentHighlights}
return (
  <>
    {/* ... */}
    {(product.options || []).map((option) => {
      const payloadOption = product.payload_product?.options?.find(
        (o) => o.medusa_id === option.id
      )
      return (
        <div key={option.id}>
          <OptionSelect
            option={option}
            current={options[option.id]}
            updateOption={setOptionValue}
            title={payloadOption?.title || option.title || ""}
            data-testid="product-options"
            disabled={!!disabled || isAdding}
          />
        </div>
      )
    })}
    {/* ... */}
  </>
)
```

You change the `title` prop of the `OptionSelect` component to use the title from Payload if available, or the Medusa option title if not.

Now, the product options and values will be displayed using the data from Payload, if available.

### Test Storefront Customization

To test out the storefront customization, make sure that both the Medusa application and the Next.js Starter Storefront are running.

Then, open the storefront at `localhost:8000` and click on Menu -> Store. In the products listing page, you'll see thumbnails and titles of the products from Payload.

![Product listing page in the storefront showing details from Payload](https://res.cloudinary.com/dza7lstvk/image/upload/v1754491849/Medusa%20Resources/CleanShot_2025-08-06_at_17.50.34_2x_pj0hjs.png)

If you click on a product, you'll see the product details page with the product title, description, images, and options from Payload.

![Product details page in the storefront showing details from Payload](https://res.cloudinary.com/dza7lstvk/image/upload/v1754491898/Medusa%20Resources/CleanShot_2025-08-06_at_17.51.28_2x_eokeyg.png)

***

## Step 9: Handle Medusa Product Events

In this step, you'll create subscribers and workflows to handle the following Medusa product events:

- [product.deleted](#a-handle-product-deletions): Delete the product in Payload when a product is deleted in Medusa.
- [product-variant.created](#b-handle-product-variant-creation): Add a product variant to a product in Payload when a product variant is created in Medusa.
- [product-variant.updated](#c-handle-product-variant-updates): Update a product variant's option values in Payload when a product variant is updated in Medusa.
- [product-variant.deleted](#d-handle-product-variant-deletions): Remove a product's variant in Payload when a product variant is deleted in Medusa.
- [product-option.created](#e-handle-product-option-creation): Add a product option to a product in Payload when a product option is created in Medusa.
- [product-option.deleted](#f-handle-product-option-deletions): Remove a product's option in Payload when a product option is deleted in Medusa.

### a. Handle Product Deletions

To handle the `product.deleted` event, you'll create a workflow that deletes the product from Payload, then create a subscriber that executes the workflow when the event is emitted.

The workflow will have the following steps:

- [deletePayloadItemsStep](#deletePayloadItemsStep): Delete the product from Payload

#### deletePayloadItemsStep

First, you need to create the `deletePayloadItemsStep` that allows you to delete items from a Payload collection.

Create the file `src/workflows/steps/delete-payload-items.ts` with the following content:

```ts title="src/workflows/steps/delete-payload-items.ts" badgeLabel="Medusa application" badgeColor="green"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PAYLOAD_MODULE } from "../../modules/payload"

type StepInput = {
  collection: string;
  where: Record<string, any>;
}

export const deletePayloadItemsStep = createStep(
  "delete-payload-items",
  async ({ where, collection }: StepInput, { container }) => {
    const payloadModuleService = container.resolve(PAYLOAD_MODULE)

    const prevData = await payloadModuleService.find(collection, {
      where,
    })

    await payloadModuleService.delete(collection, {
      where,
    })

    return new StepResponse({}, {
      prevData,
      collection,
    })
  },
  async (data, { container }) => {
    if (!data) {
      return
    }
    const { prevData, collection } = data

    const payloadModuleService = container.resolve(PAYLOAD_MODULE)

    for (const item of prevData.docs) {
      await payloadModuleService.create(
        collection,
        item
      )
    }
  }
)
```

This step accepts a collection slug and a `where` condition to specify which items to delete from Payload.

In the step, you first retrieve the existing items that match the `where` condition using the `find` method in the Payload Module's service. You pass these items to the compensation function so that you can restore them if an error occurs in the workflow.

Then, you delete the items using the `delete` method of the Payload Module's service.

#### Delete Payload Products Workflow

Next, to create the workflow that deletes products from Payload, create the file `src/workflows/delete-payload-products.ts` with the following content:

```ts title="src/workflows/delete-payload-products.ts" badgeLabel="Medusa application" badgeColor="green"
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { deletePayloadItemsStep } from "./steps/delete-payload-items"

type WorkflowInput = {
  product_ids: string[]
}

export const deletePayloadProductsWorkflow = createWorkflow(
  "delete-payload-products",
  ({ product_ids }: WorkflowInput) => {
    const deleteProductsData = transform({
      product_ids,
    }, (data) => {
      return {
        collection: "products",
        where: {
          medusa_id: {
            in: data.product_ids.join(","),
          },
        },
      }
    })

    deletePayloadItemsStep(deleteProductsData)

    return new WorkflowResponse(void 0)
  }
)
```

This workflow receives the IDs of the products to delete from Payload.

In the workflow, you prepare the data to delete from Payload using the `transform` function, then call the `deletePayloadItemsStep` to delete the products from Payload where the `medusa_id` matches one of the provided product IDs.

#### Product Deleted Subscriber

Finally, you'll create the subscriber that executes the workflow when the `product.deleted` event is emitted.

Create the file `src/subscribers/product-deleted.ts` with the following content:

```ts title="src/subscribers/product-deleted.ts" badgeLabel="Medusa application" badgeColor="green"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { deletePayloadProductsWorkflow } from "../workflows/delete-payload-products"

export default async function productDeletedHandler({
  event: { data },
  container,
}: SubscriberArgs<{
  id: string
}>) {
  await deletePayloadProductsWorkflow(container)
    .run({
      input: {
        product_ids: [data.id],
      },
    })
}

export const config: SubscriberConfig = {
  event: "product.deleted",
}
```

This subscriber listens to the `product.deleted` event and executes the `deletePayloadProductsWorkflow` with the deleted product's ID.

#### Test Product Deletion Handling

To test the product deletion handling, make sure that both the Medusa application and the Next.js Starter Storefront are running.

Then, open the Medusa Admin at `localhost:9000/app` and go to the products list. Delete a product that exists in Payload.

If you check the Products collection in Payload, you should see that the product has been removed from there as well.

### b. Handle Product Variant Creation

To handle the `product-variant.created` event, you'll create a workflow that adds the new variant to the corresponding product in Payload.

The workflow will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the product variant from Medusa

You only need to create the `updatePayloadItemsStep` step.

#### updatePayloadItemsStep

The `updatePayloadItemsStep` will update an item in a Payload collection, such as `Products`.

To create the step, create the file `src/workflows/steps/update-payload-items.ts` with the following content:

```ts title="src/workflows/steps/update-payload-items.ts" badgeLabel="Medusa application" badgeColor="green"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PayloadItemResult, PayloadUpsertData } from "../../modules/payload/types"
import { PAYLOAD_MODULE } from "../../modules/payload"

type StepInput = {
  collection: string;
  items: PayloadUpsertData[];
}

export const updatePayloadItemsStep = createStep(
  "update-payload-items",
  async ({ items, collection }: StepInput, { container }) => {
    const payloadModuleService = container.resolve(PAYLOAD_MODULE)
    const ids: string[] = items.map((item) => item.id)

    const prevData = await payloadModuleService.find(collection, {
      where: {
        id: {
          in: ids.join(","),
        },
      },
    })

    const updatedItems: PayloadItemResult[] = []

    for (const item of items) {
      const { id, ...data } = item
      updatedItems.push(
        await payloadModuleService.update(
          collection,
          data,
          {
            where: {
              id: {
                equals: id,
              },
            },
          }
        )
      )
    }

    return new StepResponse({
      items: updatedItems.map((item) => item.doc),
    }, {
      prevData,
      collection,
    })
  },
  async (data, { container }) => {
    if (!data) {
      return
    }
    const { prevData, collection } = data

    const payloadModuleService = container.resolve(PAYLOAD_MODULE)

    await Promise.all(
      prevData.docs.map(async ({
        id,
        ...item
      }) => {
        await payloadModuleService.update(
          collection,
          item,
          {
            where: {
              id: {
                equals: id,
              },
            },
          }
        )
      })
    )
  }
)
```

In the step function, you retrieve the existing data from Payload to pass it to the compensation function. Then, you update the items in Payload.

In the compensation function, you revert the changes made in the step function if an error occurs.

#### Create Payload Product Variant Workflow

Create the file `src/workflows/create-payload-product-variant.ts` with the following content:

```ts title="src/workflows/create-payload-product-variant.ts" badgeLabel="Medusa application" badgeColor="green"
import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { PayloadCollectionItem, PayloadUpsertData } from "../modules/payload/types"
import { updatePayloadItemsStep } from "./steps/update-payload-items"

type WorkflowInput = {
  variant_ids: string[]; 
}

export const createPayloadProductVariantWorkflow = createWorkflow(
  "create-payload-product-variant",
  ({ variant_ids }: WorkflowInput) => {
    const { data: productVariants } = useQueryGraphStep({
      entity: "product_variant",
      fields: [
        "id",
        "title",
        "options.*",
        "options.option.*",
        "product.payload_product.*",
      ],
      filters: {
        id: variant_ids,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const updateData = transform({
      productVariants,
    }, (data) => {
      const items: Record<string, PayloadUpsertData> = {}

      data.productVariants.forEach((variant) => {
        // @ts-expect-error
        const payloadProduct = variant.product?.payload_product as PayloadCollectionItem
        if (!payloadProduct) {return}
        if (!items[payloadProduct.id]) {
          items[payloadProduct.id] = {
            variants: payloadProduct.variants || [],
          }
        }

        items[payloadProduct.id].variants.push({
          title: variant.title,
          medusa_id: variant.id,
          option_values: variant.options.map((option) => ({
            medusa_id: option.id,
            medusa_option_id: option.option?.id,
            value: option.value,
          })),
        })
      })
      
      return {
        collection: "products",
        items: Object.keys(items).map((id) => ({
          id,
          ...items[id],
        })),
      }
    })

    const result = when({ updateData }, (data) => data.updateData.items.length > 0)
      .then(() => {
        return updatePayloadItemsStep(updateData)
      })

    const items = transform({ result }, (data) => data.result?.items || [])

    return new WorkflowResponse({
      items,
    })
  }
)
```

This workflow receives the IDs of the product variants to add to Payload.

In the workflow, you:

1. Retrieve the product variant details from Medusa using the `useQueryGraphStep`, including the linked product data from Payload.
2. Prepare the data to update the product in Payload by adding the new variant to the existing variants array.
3. Update the product in Payload using the `updatePayloadItemsStep` if there are any items to update.
4. Return the updated items from the workflow.

#### Product Variant Created Subscriber

Finally, you'll create the subscriber that executes the workflow when the `product-variant.created` event is emitted.

Create the file `src/subscribers/variant-created.ts` with the following content:

```ts title="src/subscribers/variant-created.ts" badgeLabel="Medusa application" badgeColor="green"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { createPayloadProductVariantWorkflow } from "../workflows/create-payload-product-variant"

export default async function productVariantCreatedHandler({
  event: { data },
  container,
}: SubscriberArgs<{
  id: string
}>) {
  await createPayloadProductVariantWorkflow(container)
    .run({
      input: {
        variant_ids: [data.id],
      },
    })
}

export const config: SubscriberConfig = {
  event: "product-variant.created",
}
```

This subscriber listens to the `product-variant.created` event and executes the `createPayloadProductVariantWorkflow` with the created variant's ID.

#### Test Product Variant Creation Handling

To test the product variant creation handling, make sure that both the Medusa application and the Next.js Starter Storefront are running.

Then, open the Medusa Admin at `localhost:9000/app` and open a product's details page. Add a new variant to the product and save the changes.

If you check the product in Payload, you should see that the new variant has been added to the product's variants array.

### c. Handle Product Variant Updates

To handle the `product-variant.updated` event, you'll create a workflow that updates the variant in the corresponding product in Payload.

The workflow will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the product variant from Medusa

#### Update Payload Product Variants Workflow

Since you already have the necessary steps, you only need to create the workflow that uses these steps.

Create the file `src/workflows/update-payload-product-variants.ts` with the following content:

```ts title="src/workflows/update-payload-product-variants.ts" badgeLabel="Medusa application" badgeColor="green"
import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { PayloadCollectionItem, PayloadUpsertData } from "../modules/payload/types"
import { updatePayloadItemsStep } from "./steps/update-payload-items"

type WorkflowInput = {
  variant_ids: string[]; 
}

export const updatePayloadProductVariantsWorkflow = createWorkflow(
  "update-payload-product-variants",
  ({ variant_ids }: WorkflowInput) => {
    const { data: productVariants } = useQueryGraphStep({
      entity: "product_variant",
      fields: [
        "id",
        "title",
        "options.*",
        "options.option.*",
        "product.payload_product.*",
      ],
      filters: {
        id: variant_ids,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const updateData = transform({
      productVariants,
    }, (data) => {
      const items: Record<string, PayloadUpsertData> = {}

      data.productVariants.forEach((variant) => {
        // @ts-expect-error
        const payloadProduct = variant.product?.payload_product as PayloadCollectionItem
        if (!payloadProduct) {return}
        
        if (!items[payloadProduct.id]) {
          items[payloadProduct.id] = {
            variants: payloadProduct.variants || [],
          }
        }

        // Find and update the existing variant in the payload product
        const existingVariantIndex = items[payloadProduct.id].variants.findIndex(
          (v: any) => v.medusa_id === variant.id
        )

        if (existingVariantIndex >= 0) {
          // check if option values need to be updated
          const existingVariant = items[payloadProduct.id].variants[existingVariantIndex]
          const updatedOptionValues = variant.options.map((option) => ({
            medusa_id: option.id,
            medusa_option_id: option.option?.id,
            value: existingVariant.option_values.find((ov: any) => ov.medusa_id === option.id)?.value || 
              option.value,
          }))

          items[payloadProduct.id].variants[existingVariantIndex] = {
            ...existingVariant,
            option_values: updatedOptionValues,
          }
        } else {
          // Add the new variant to the payload product
          items[payloadProduct.id].variants.push({
            title: variant.title,
            medusa_id: variant.id,
            option_values: variant.options.map((option) => ({
              medusa_id: option.id,
              medusa_option_id: option.option?.id,
              value: option.value,
            })),
          })
        }
      })
      
      return {
        collection: "products",
        items: Object.keys(items).map((id) => ({
          id,
          ...items[id],
        })),
      }
    })

    const result = when({ updateData }, (data) => data.updateData.items.length > 0)
      .then(() => {
        return updatePayloadItemsStep(updateData)
      })

    const items = transform({ result }, (data) => data.result?.items || [])

    return new WorkflowResponse({
      items,
    })
  }
)
```

This workflow receives the IDs of the product variants to update in Payload.

In the workflow, you:

1. Retrieve the product variant details from Medusa using the `useQueryGraphStep`, including the linked product data from Payload.
2. Prepare the data to update the product in Payload by finding and updating the existing variant in the variants array. You only update the variant's option values, in case a new one is added.
3. Update the product in Payload using the `updatePayloadItemsStep` if there are any items to update.
4. Return the updated items from the workflow.

#### Product Variant Updated Subscriber

Finally, you'll create the subscriber that executes the workflow.

Create the file `src/subscribers/variant-updated.ts` with the following content:

```ts title="src/subscribers/variant-updated.ts" badgeLabel="Medusa application" badgeColor="green"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { updatePayloadProductVariantsWorkflow } from "../workflows/update-payload-product-variants"

export default async function productVariantUpdatedHandler({
  event: { data },
  container,
}: SubscriberArgs<{
  id: string
}>) {
  await updatePayloadProductVariantsWorkflow(container)
    .run({
      input: {
        variant_ids: [data.id],
      },
    })
}

export const config: SubscriberConfig = {
  event: "product-variant.updated",
}
```

This subscriber listens to the `product-variant.updated` event and executes the `updatePayloadProductVariantsWorkflow` with the updated variant's ID.

#### Test Product Variant Update Handling

To test the product variant update handling, make sure that both the Medusa application and the Next.js Starter Storefront are running.

Then, open the Medusa Admin at `localhost:9000/app` and open a product's details page. Edit an existing variant's title and save the changes.

If you check the product in Payload, you should see that the variant's option values have been updated in the product's variants array.

### d. Handle Product Variant Deletions

To handle the `product-variant.deleted` event, you'll create a workflow that removes the variant from the corresponding product in Payload.

The workflow will have the following steps:

- [retrievePayloadItemsStep](#retrievePayloadItemsStep): Retrieve the products containing the variant from Payload

#### retrievePayloadItemsStep

Since the `deletePayloadProductVariantsWorkflow` is executed after a product variant is deleted, you can't retrieve the product variant data from Medusa.

Instead, you'll create a step that retrieves the products containing the variants from Payload. You'll then use this data to update the products in Payload.

To create the step, create the file `src/workflows/steps/retrieve-payload-items.ts` with the following content:

```ts title="src/workflows/steps/retrieve-payload-items.ts" badgeLabel="Medusa application" badgeColor="green"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { PAYLOAD_MODULE } from "../../modules/payload"

type StepInput = {
  collection: string;
  where: Record<string, any>;
}

export const retrievePayloadItemsStep = createStep(
  "retrieve-payload-items",
  async ({ where, collection }: StepInput, { container }) => {
    const payloadModuleService = container.resolve(PAYLOAD_MODULE)

    const items = await payloadModuleService.find(collection, {
      where,
    })

    return new StepResponse({
      items: items.docs,
    })
  }
)
```

This step accepts a collection slug and a `where` condition to specify which items to retrieve from Payload, then returns the found items.

#### Delete Payload Product Variants Workflow

To create the workflow, create the file `src/workflows/delete-payload-product-variants.ts` with the following content:

```ts title="src/workflows/delete-payload-product-variants.ts" badgeLabel="Medusa application" badgeColor="green"
import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { updatePayloadItemsStep } from "./steps/update-payload-items"
import { retrievePayloadItemsStep } from "./steps/retrieve-payload-items"

type WorkflowInput = {
  variant_ids: string[]
}

export const deletePayloadProductVariantsWorkflow = createWorkflow(
  "delete-payload-product-variants",
  ({ variant_ids }: WorkflowInput) => {
    const retrieveData = transform({
      variant_ids,
    }, (data) => {
      return {
        collection: "products",
        where: {
          "variants.medusa_id": {
            in: data.variant_ids.join(","),
          },
        },
      }
    })

    const { items: payloadProducts } = retrievePayloadItemsStep(retrieveData)

    const updateData = transform({
      payloadProducts,
      variant_ids,
    }, (data) => {
      const items = data.payloadProducts.map((payloadProduct) => ({
        id: payloadProduct.id,
        variants: payloadProduct.variants.filter((v: any) => !data.variant_ids.includes(v.medusa_id)),
      }))
      
      return {
        collection: "products",
        items,
      }
    })

    const result = when({ updateData }, (data) => data.updateData.items.length > 0)
      .then(() => {
        // Call the step to update the payload items
        return updatePayloadItemsStep(updateData)
      })

    const items = transform({ result }, (data) => data.result?.items || [])

    return new WorkflowResponse({
      items,
    })
  }
)
```

This workflow receives the IDs of the product variants to delete from Payload.

In the workflow, you:

1. Retrieve the Payload data of the products that the variants belong to using `retrievePayloadItemsStep`.
2. Prepare the data to update the products in Payload by filtering out the variants that should be deleted.
3. Update the products in Payload using the `updatePayloadItemsStep` if there are any items to update.
4. Return the updated items from the workflow.

#### Product Variant Deleted Subscriber

Finally, you'll create the subscriber that executes the workflow when the `product-variant.deleted` event is emitted.

Create the file `src/subscribers/variant-deleted.ts` with the following content:

```ts title="src/subscribers/variant-deleted.ts" badgeLabel="Medusa application" badgeColor="green"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { deletePayloadProductVariantsWorkflow } from "../workflows/delete-payload-product-variants"

export default async function productVariantDeletedHandler({
  event: { data },
  container,
}: SubscriberArgs<{
  id: string
}>) {
  await deletePayloadProductVariantsWorkflow(container)
    .run({
      input: {
        variant_ids: [data.id],
      },
    })
}

export const config: SubscriberConfig = {
  event: "product-variant.deleted",
}
```

This subscriber listens to the `product-variant.deleted` event and executes the `deletePayloadProductVariantsWorkflow` with the deleted variant's ID.

#### Test Product Variant Deletion Handling

To test the product variant deletion handling, make sure that both the Medusa application and the Next.js Starter Storefront are running.

Then, open the Medusa Admin at `localhost:9000/app` and open a product's details page. Delete an existing variant from the product.

If you check the product in Payload, you should see that the variant has been removed from the product's variants array.

### e. Handle Product Option Creation

To handle the `product-option.created` event, you'll create a workflow that adds the new option to the corresponding product in Payload.

The workflow will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the product option from Medusa

#### Create Payload Product Options Workflow

You already have the necessary steps, so you only need to create the workflow that uses these steps.

Create the file `src/workflows/create-payload-product-options.ts` with the following content:

```ts title="src/workflows/create-payload-product-options.ts" badgeLabel="Medusa application" badgeColor="green"
import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { PayloadCollectionItem, PayloadUpsertData } from "../modules/payload/types"
import { updatePayloadItemsStep } from "./steps/update-payload-items"

type WorkflowInput = {
  option_ids: string[]; 
}

export const createPayloadProductOptionsWorkflow = createWorkflow(
  "create-payload-product-options",
  ({ option_ids }: WorkflowInput) => {
    const { data: productOptions } = useQueryGraphStep({
      entity: "product_option",
      fields: [
        "id",
        "title",
        "product.payload_product.*",
      ],
      filters: {
        id: option_ids,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const updateData = transform({
      productOptions,
    }, (data) => {
      const items: Record<string, PayloadUpsertData> = {}

      data.productOptions.forEach((option) => {
        // @ts-expect-error
        const payloadProduct = option.product?.payload_product as PayloadCollectionItem
        if (!payloadProduct) {return}
        
        if (!items[payloadProduct.id]) {
          items[payloadProduct.id] = {
            options: payloadProduct.options || [],
          }
        }

        // Add the new option to the payload product
        const newOption = {
          title: option.title,
          medusa_id: option.id,
        }

        // Check if option already exists, if not add it
        const existingOptionIndex = items[payloadProduct.id].options.findIndex(
          (o: any) => o.medusa_id === option.id
        )
        
        if (existingOptionIndex === -1) {
          items[payloadProduct.id].options.push(newOption)
        }
      })
      
      return {
        collection: "products",
        items: Object.keys(items).map((id) => ({
          id,
          ...items[id],
        })),
      }
    })

    const result = when({ updateData }, (data) => data.updateData.items.length > 0)
      .then(() => {
        return updatePayloadItemsStep(updateData)
      })

    const items = transform({ result }, (data) => data.result?.items || [])

    return new WorkflowResponse({
      items,
    })
  }
)
```

This workflow receives the IDs of the product options to add to Payload.

In the workflow, you:

1. Retrieve the product option details from Medusa using the `useQueryGraphStep`, including the linked product data from Payload.
2. Prepare the data to update the product in Payload by adding the new option to the existing options array, checking if it doesn't already exist.
3. Update the product in Payload using the `updatePayloadItemsStep` if there are any items to update.
4. Return the updated items from the workflow.

#### Product Option Created Subscriber

Finally, you'll create the subscriber that executes the workflow when the `product-option.created` event is emitted.

Create the file `src/subscribers/option-created.ts` with the following content:

```ts title="src/subscribers/option-created.ts" badgeLabel="Medusa application" badgeColor="green"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { createPayloadProductOptionsWorkflow } from "../workflows/create-payload-product-options"

export default async function productOptionCreatedHandler({
  event: { data },
  container,
}: SubscriberArgs<{
  id: string
}>) {
  await createPayloadProductOptionsWorkflow(container)
    .run({
      input: {
        option_ids: [data.id],
      },
    })
}

export const config: SubscriberConfig = {
  event: "product-option.created",
}
```

This subscriber listens to the `product-option.created` event and executes the `createPayloadProductOptionsWorkflow` with the created option's ID.

#### Test Product Option Creation Handling

To test the product option creation handling, make sure that both the Medusa application and the Next.js Starter Storefront are running.

Then, open the Medusa Admin at `localhost:9000/app` and open a product's details page. Add a new option to the product and save the changes.

If you check the product in Payload, you should see that the new option has been added to the product's options array.

### f. Handle Product Option Deletions

To handle the `product-option.deleted` event, you'll create a workflow that removes the option from the corresponding product in Payload.

The workflow will have the following steps:

- [retrievePayloadItemsStep](#retrievePayloadItemsStep): Retrieve the products containing the option from Payload

#### Delete Payload Product Options Workflow

You already have the necessary steps, so you only need to create the workflow that uses these steps.

Create the file `src/workflows/delete-payload-product-options.ts` with the following content:

```ts title="src/workflows/delete-payload-product-options.ts" badgeLabel="Medusa application" badgeColor="green"
import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { updatePayloadItemsStep } from "./steps/update-payload-items"
import { retrievePayloadItemsStep } from "./steps/retrieve-payload-items"

type WorkflowInput = {
  option_ids: string[]
}

export const deletePayloadProductOptionsWorkflow = createWorkflow(
  "delete-payload-product-options",
  ({ option_ids }: WorkflowInput) => {
    const retrieveData = transform({
      option_ids,
    }, (data) => {
      return {
        collection: "products",
        where: {
          "options.medusa_id": {
            in: data.option_ids.join(","),
          },
        },
      }
    })

    const { items: payloadProducts } = retrievePayloadItemsStep(retrieveData)

    const updateData = transform({
      payloadProducts,
      option_ids,
    }, (data) => {
      const items = data.payloadProducts.map((payloadProducts) => ({
        id: payloadProducts.id,
        options: payloadProducts.options.filter((o: any) => !data.option_ids.includes(o.medusa_id)),
        variants: payloadProducts.variants.map((variant: any) => ({
          ...variant,
          option_values: variant.option_values.filter((ov: any) => !data.option_ids.includes(ov.medusa_option_id)),
        })),
      }))
      
      return {
        collection: "products",
        items,
      }
    })

    const result = when({ updateData }, (data) => data.updateData.items.length > 0)
      .then(() => {
        return updatePayloadItemsStep(updateData)
      })

    const items = transform({ result }, (data) => data.result?.items || [])

    return new WorkflowResponse({
      items,
    })
  }
)
```

This workflow receives the IDs of the product options to delete from Payload.

In the workflow, you:

1. Retrieve the products that contain the options to be deleted using the `retrievePayloadItemsStep`.
2. Prepare the data to update the products in Payload by filtering out the options that should be deleted.
3. Update the products in Payload using the `updatePayloadItemsStep` if there are any items to update.
4. Return the updated items from the workflow.

#### Product Option Deleted Subscriber

Finally, you'll create the subscriber that executes the workflow when the `product-option.deleted` event is emitted.

Create the file `src/subscribers/option-deleted.ts` with the following content:

```ts title="src/subscribers/option-deleted.ts" badgeLabel="Medusa application" badgeColor="green"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { deletePayloadProductOptionsWorkflow } from "../workflows/delete-payload-product-options"

export default async function productOptionDeletedHandler({
  event: { data },
  container,
}: SubscriberArgs<{
  id: string
}>) {
  await deletePayloadProductOptionsWorkflow(container)
    .run({
      input: {
        option_ids: [data.id],
      },
    })
}

export const config: SubscriberConfig = {
  event: "product-option.deleted",
}
```

This subscriber listens to the `product-option.deleted` event and executes the `deletePayloadProductOptionsWorkflow` with the deleted option's ID.

#### Test Product Option Deletion Handling

To test the product option deletion handling, make sure that both the Medusa application and the Next.js Starter Storefront are running.

Then, open the Medusa Admin at `localhost:9000/app` and open a product's details page. Delete an existing option from the product.

If you check the product in Payload, you should see that the option has been removed from the product's options array.

***

## Next Steps

You've successfully integrated Medusa with Payload to manage content related to products, variants, and options. You can expand on this integration by adding more features, such as:

1. Managing the content of other entities, like categories or collections. The process is similar to what you've done for products:
   1. Create a collection in Payload for the entity.
   2. Create Medusa workflows and subscribers to handle the creation, update, and deletion of the entity.
   3. Display the payload data in your Next.js Starter Storefront.
2. Enable [localization](https://payloadcms.com/docs/configuration/localization) in Payload to support multiple languages.
   - You only need to manage the localized content in Payload. Only the default locale will be synced with Medusa.
   - You can show the localized content in your Next.js Starter Storefront based on the customer's locale.
3. Add custom fields to the Payload collections that are relevant for the storefront, such as SEO metadata or promotional banners.

### Learn More about Medusa

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.


# Integrate PayPal (Payment) with Medusa

In this tutorial, you'll learn how to integrate PayPal with Medusa for payment processing.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. Medusa's architecture facilitates integrating third-party services to handle various functionalities, including payment processing.

[PayPal](https://www.paypal.com/) is a widely used payment gateway that allows businesses to accept payments online securely. By integrating PayPal with Medusa, you can offer your customers a convenient and trusted payment option.

## Summary

By following this tutorial, you'll learn how to:

- Install and set up Medusa.
- Integrate PayPal as a Payment Module Provider in Medusa.
- Customize the Next.js Starter Storefront to include PayPal as a payment option.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Diagram showing the checkout flow between customer, Medusa, and PayPal](https://res.cloudinary.com/dza7lstvk/image/upload/v1765268705/Medusa%20Resources/paypal-overview_rsqisb.jpg)

[Full Code](https://github.com/medusajs/examples/tree/main/paypal-integration): Find the complete code for this integration on GitHub.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST API endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create PayPal Module Provider

To integrate third-party services into Medusa, you create a custom module. A module is a reusable package with functionalities related to a single feature or domain.

Medusa's [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md) provides an interface to process payments in your Medusa application. It delegates the actual payment processing to the underlying providers.

In this step, you'll integrate PayPal as a Payment Module Provider and configure it in your Medusa application. Later, you'll use it to process payments.

Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules in Medusa.

### a. Install PayPal SDK

To interact with PayPal's APIs, run the following command in your Medusa application to install the PayPal server SDK:

```bash npm2yarn
npm install @paypal/paypal-server-sdk
```

You'll use the SDK in the Payment Module Provider's service.

### b. Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/paypal`.

### c. Create PayPal Module's Service

A module has a service that contains its logic. For Payment Module Providers, the service implements the logic to process payments with third-party services.

To create the service of the PayPal Payment Module Provider, create the file `src/modules/paypal/service.ts` with the following content:

```ts title="src/modules/paypal/service.ts"
import { AbstractPaymentProvider } from "@medusajs/framework/utils"
import { Logger } from "@medusajs/framework/types"
import {
  Client,
  Environment,
  OrdersController,
  PaymentsController,
} from "@paypal/paypal-server-sdk"

type Options = {
  client_id: string
  client_secret: string
  environment?: "sandbox" | "production"
  autoCapture?: boolean
  webhook_id?: string
}

type InjectedDependencies = {
  logger: Logger
}

class PayPalPaymentProviderService extends AbstractPaymentProvider<Options> {
  static identifier = "paypal"

  protected logger_: Logger
  protected options_: Options
  protected client_: Client
  protected ordersController_: OrdersController
  protected paymentsController_: PaymentsController

  constructor(container: InjectedDependencies, options: Options) {
    super(container, options)

    this.logger_ = container.logger
    this.options_ = {
      environment: "sandbox",
      autoCapture: false,
      ...options,
    }

    // Initialize PayPal client
    this.client_ = new Client({
      environment:
        this.options_.environment === "production"
          ? Environment.Production
          : Environment.Sandbox,
      clientCredentialsAuthCredentials: {
        oAuthClientId: this.options_.client_id,
        oAuthClientSecret: this.options_.client_secret,
      },
    })

    this.ordersController_ = new OrdersController(this.client_)
    this.paymentsController_ = new PaymentsController(this.client_)
  }

  // TODO: Add methods
}

export default PayPalPaymentProviderService
```

A Payment Module Provider service must extend the `AbstractPaymentProvider` class. It must also have a static `identifier` property that uniquely identifies the provider.

The module provider's constructor receives two parameters:

- `container`: The [module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) that contains Framework resources available to the module.
- `options`: Options that are passed to the module provider when it's registered in Medusa's configurations. You define the following option:
  - `client_id`: The PayPal Client ID.
  - `client_secret`: The PayPal Client Secret.
  - `environment`: The PayPal environment to use, either `sandbox` or `production`.
  - `autoCapture`: Whether to capture payments immediately or authorize them for later capture.
  - `webhook_id`: The PayPal Webhook ID for validating webhooks.

In the constructor, you initialize the PayPal SDK client and controllers using the provided credentials.

In the next sections, you'll implement the methods required by the `AbstractPaymentProvider` class to process payments with PayPal.

Refer to the [Create Payment Module Provider](https://docs.medusajs.com/references/payment/provider/index.html.md) guide for detailed information about the methods.

#### validateOptions Method

The [validateOptions](https://docs.medusajs.com/references/payment/provider#validateoptions/index.html.md) method validates that the module has received the required options.

Add the following method to the `PayPalPaymentProviderService` class:

```ts title="src/modules/paypal/service.ts"
import { 
  MedusaError
} from "@medusajs/framework/utils"

class PayPalPaymentProviderService extends AbstractPaymentProvider<Options> {
  // ...
  static validateOptions(options: Record<any, any>): void | never {
    if (!options.client_id) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA, 
        "Client ID is required"
      )
    }
    if (!options.client_secret) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA, 
        "Client secret is required"
      )
    }
  }
}
```

The `validateOptions` method receives the options passed to the module provider as a parameter.

In the method, you throw an error if the `client_id` or `client_secret` options are missing. This will stop the application from starting.

#### initiatePayment Method

The [initiatePayment](https://docs.medusajs.com/references/payment/provider#initiatepayment/index.html.md) method initializes a payment session with the third-party service. It's called when the customer selects a payment method during checkout.

You'll create a PayPal order in this method.

Add the following method to the `PayPalPaymentProviderService` class:

```ts title="src/modules/paypal/service.ts"
import type {
  InitiatePaymentInput,
  InitiatePaymentOutput,
} from "@medusajs/framework/types"
import {
  CheckoutPaymentIntent,
  OrderApplicationContextLandingPage,
  OrderApplicationContextUserAction,
  OrderRequest,
} from "@paypal/paypal-server-sdk"

class PayPalPaymentProviderService extends AbstractPaymentProvider<Options> {
  // ...
  async initiatePayment(
    input: InitiatePaymentInput
  ): Promise<InitiatePaymentOutput> {
    try {
      const { amount, currency_code } = input

      // Determine intent based on autoCapture option
      const intent = this.options_.autoCapture
        ? CheckoutPaymentIntent.Capture
        : CheckoutPaymentIntent.Authorize

      // Create PayPal order request
      const orderRequest: OrderRequest = {
        intent: intent,
        purchaseUnits: [
          {
            amount: {
              currencyCode: currency_code.toUpperCase(),
              value: amount.toString(),
            },
            description: "Order payment",
            customId: input.data?.session_id as string | undefined,
          },
        ],
        applicationContext: {
          // TODO: Customize as needed
          brandName: "Store",
          landingPage: OrderApplicationContextLandingPage.NoPreference,
          userAction: OrderApplicationContextUserAction.PayNow,
        },
      }

      const response = await this.ordersController_.createOrder({
        body: orderRequest,
        prefer: "return=representation",
      })

      const order = response.result

      if (!order?.id) {
        throw new MedusaError(
          MedusaError.Types.UNEXPECTED_STATE,
          "Failed to create PayPal order"
        )
      }

      // Extract approval URL from links
      const approvalUrl = order.links?.find(
        (link) => link.rel === "approve"
      )?.href

      return {
        id: order.id,
        data: {
          order_id: order.id,
          intent: intent,
          status: order.status,
          approval_url: approvalUrl,
          session_id: input.data?.session_id,
          currency_code
        },
      }
    } catch (error: any) {
      throw new MedusaError(
        MedusaError.Types.UNEXPECTED_STATE,
        `Failed to initiate PayPal payment: ${error.result?.message || error}`
      )
    }
  }
}
```

The `initiatePayment` method receives an object with the payment details, such as the amount and currency code.

In the method, you:

1. Determine the payment intent based on the `autoCapture` option. By default, payments are authorized for later capture.
2. Create a PayPal order request with the payment details.
   - You can customize the `applicationContext` as needed. For example, you can set your store's name and the PayPal landing page type.

You return an object with the PayPal order ID and a `data` object with additional information, such as the intent and approval URL. Medusa stores the `data` object in the payment session's `data` field, allowing you to access it for later processing.

Refer to the [Create Payment Module Provider](https://docs.medusajs.com/references/payment/provider#initiatepayment/index.html.md) guide for detailed information about this method.

#### authorizePayment Method

The [authorizePayment](https://docs.medusajs.com/references/payment/provider#authorizepayment/index.html.md) method authorizes a payment with the third-party service. It's called when the customer places their order to authorize the payment with the selected payment method.

You'll authorize or capture the PayPal order in this method, based on the `autoCapture` option.

Add the following method to the `PayPalPaymentProviderService` class:

```ts title="src/modules/paypal/service.ts"
import type {
  AuthorizePaymentInput,
  AuthorizePaymentOutput,
  PaymentSessionStatus,
} from "@medusajs/framework/types"

class PayPalPaymentProviderService extends AbstractPaymentProvider<Options> {
  // ...
  async authorizePayment(
    input: AuthorizePaymentInput
  ): Promise<AuthorizePaymentOutput> {
    try {
      const orderId = input.data?.order_id as string | undefined

      if (!orderId || typeof orderId !== "string") {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          "PayPal order ID is required"
        )
      }

      // If autoCapture is enabled, authorize and capture in one step
      if (this.options_.autoCapture) {
        const response = await this.ordersController_.captureOrder({
          id: orderId,
          prefer: "return=representation",
        })

        const capture = response.result

        if (!capture?.id) {
          throw new MedusaError(
            MedusaError.Types.UNEXPECTED_STATE,
            "Failed to capture PayPal payment"
          )
        }

        // Extract capture ID from purchase units
        const captureId =
          capture.purchaseUnits?.[0]?.payments?.captures?.[0]?.id

        return {
          data: {
            ...input.data,
            capture_id: captureId,
            intent: "CAPTURE",
          },
          status: "captured" as PaymentSessionStatus,
        }
      }

      // Otherwise, just authorize
      const response = await this.ordersController_.authorizeOrder({
        id: orderId,
        prefer: "return=representation",
      })

      const authorization = response.result

      if (!authorization?.id) {
        throw new MedusaError(
          MedusaError.Types.UNEXPECTED_STATE,
          "Failed to authorize PayPal payment"
        )
      }

      // Extract authorization ID from purchase units
      const authId =
        authorization.purchaseUnits?.[0]?.payments?.authorizations?.[0]?.id

      return {
        data: {
          order_id: orderId,
          authorization_id: authId,
          intent: "AUTHORIZE",
          currency_code: input.data?.currency_code,
        },
        status: "authorized" as PaymentSessionStatus,
      }
    } catch (error: any) {
      throw new MedusaError(
        MedusaError.Types.UNEXPECTED_STATE,
        `Failed to authorize PayPal payment: ${error.message || error}`
      )
    }
  }
}
```

The `authorizePayment` method receives an object with the payment session's `data` field.

In the method, you:

1. Extract the PayPal order ID from the `data` field. This is the same `data.order_id` you returned in the `initiatePayment` method.
2. If the `autoCapture` option is enabled, you capture the PayPal order.
3. Otherwise, you authorize the PayPal order.

You return an object with a `data` field containing additional information, such as the authorization or capture ID. Medusa will store the `data` object in the newly created payment record for later processing.

Refer to the [Create Payment Module Provider](https://docs.medusajs.com/references/payment/provider#authorizepayment/index.html.md) guide for detailed information about this method.

#### capturePayment Method

The [capturePayment](https://docs.medusajs.com/references/payment/provider#capturepayment/index.html.md) method captures a previously authorized payment with the third-party service. It's called either when:

- An admin user captures the payment from the Medusa Admin dashboard.
- PayPal webhook notifies Medusa that the payment has been captured.

You'll captured the authorized PayPal order in this method.

Add the following method to the `PayPalPaymentProviderService` class:

```ts title="src/modules/paypal/service.ts"
import type {
  CapturePaymentInput,
  CapturePaymentOutput,
} from "@medusajs/framework/types"

class PayPalPaymentProviderService extends AbstractPaymentProvider<Options> {
  // ...
  async capturePayment(
    input: CapturePaymentInput
  ): Promise<CapturePaymentOutput> {
    try {
      const authorizationId = input.data?.authorization_id as string | undefined

      if (!authorizationId || typeof authorizationId !== "string") {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          "PayPal authorization ID is required for capture"
        )
      }

      const response = await this.paymentsController_.captureAuthorizedPayment({
        authorizationId: authorizationId,
        prefer: "return=representation",
      })

      const capture = response.result

      if (!capture?.id) {
        throw new MedusaError(
          MedusaError.Types.UNEXPECTED_STATE,
          "Failed to capture PayPal payment"
        )
      }

      return {
        data: {
          ...input.data,
          capture_id: capture.id,
        },
      }
    } catch (error: any) {
      throw new MedusaError(
        MedusaError.Types.UNEXPECTED_STATE,
        `Failed to capture PayPal payment: ${error.result?.message || error}`
      )
    }
  }
}
```

The `capturePayment` method receives an object with the payment record's `data` field.

In the method, you:

1. Extract the PayPal authorization ID from the `data` field. This is the same `data.authorization_id` you returned in the `authorizePayment` method.
2. Capture the authorized PayPal payment.

You return an object with a `data` field containing additional information, such as the capture ID. Medusa updates the payment record's `data` field with the returned `data` object.

Refer to the [Create Payment Module Provider](https://docs.medusajs.com/references/payment/provider#capturepayment/index.html.md) guide for detailed information about this method.

#### refundPayment Method

The [refundPayment](https://docs.medusajs.com/references/payment/provider#refundpayment/index.html.md) method refunds a previously captured payment with the third-party service. It's called when an admin user issues a refund from the Medusa Admin dashboard.

You'll refund the captured PayPal payment in this method.

Add the following method to the `PayPalPaymentProviderService` class:

```ts title="src/modules/paypal/service.ts"
import type {
  RefundPaymentInput,
  RefundPaymentOutput,
} from "@medusajs/framework/types"
import { BigNumber } from "@medusajs/framework/utils"

class PayPalPaymentProviderService extends AbstractPaymentProvider<Options> {
  // ...
  async refundPayment(input: RefundPaymentInput): Promise<RefundPaymentOutput> {
    try {
      const captureId = input.data?.capture_id as string | undefined

      if (!captureId || typeof captureId !== "string") {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          "PayPal capture ID is required for refund"
        )
      }

      const refundRequest = {
        amount: {
          currencyCode: (input.data?.currency_code as string | undefined)
            ?.toUpperCase() || "",
          value: new BigNumber(input.amount).numeric.toString(),
        }
      }

      const response = await this.paymentsController_.refundCapturedPayment({
        captureId: captureId,
        body: Object.keys(refundRequest).length > 0 ? refundRequest : undefined,
        prefer: "return=representation",
      })

      const refund = response.result

      if (!refund?.id) {
        throw new MedusaError(
          MedusaError.Types.UNEXPECTED_STATE,
          "Failed to refund PayPal payment"
        )
      }

      return {
        data: {
          ...input.data,
          refund_id: refund.id,
        },
      }
    } catch (error: any) {
      throw new MedusaError(
        MedusaError.Types.UNEXPECTED_STATE,
        `Failed to refund PayPal payment: ${error.result?.message || error}`
      )
    }
  }
}
```

The `refundPayment` method receives an object with the payment record's `data` field.

In the method, you:

1. Extract the PayPal capture ID from the `data` field. This is the same `data.capture_id` you returned in the `capturePayment` method.
2. Extract the amount and currency code from the input.
3. Refund the captured PayPal payment.

You return an object with a `data` field containing additional information, such as the refund ID. Medusa updates the payment record's `data` field with the returned `data` object.

Refer to the [Create Payment Module Provider](https://docs.medusajs.com/references/payment/provider#refundpayment/index.html.md) guide for detailed information about this method.

#### updatePayment Method

The [updatePayment](https://docs.medusajs.com/references/payment/provider#updatepayment/index.html.md) method updates the payment session in the third-party service. It's called when the payment session needs to be updated, such as when the order amount changes.

You'll update the PayPal order amount in this method.

Add the following method to the `PayPalPaymentProviderService` class:

```ts title="src/modules/paypal/service.ts"
import type {
  UpdatePaymentInput,
  UpdatePaymentOutput,
} from "@medusajs/framework/types"
import {
  PatchOp,
} from "@paypal/paypal-server-sdk"

class PayPalPaymentProviderService extends AbstractPaymentProvider<Options> {
  // ...
  async updatePayment(
    input: UpdatePaymentInput
  ): Promise<UpdatePaymentOutput> {
    try {
      const orderId = input.data?.order_id as string | undefined

      if (!orderId) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          "PayPal order ID is required"
        )
      }
      
      await this.ordersController_.patchOrder({
        id: orderId as string,
        body: [
          {
            op: PatchOp.Replace,
            path: "/purchase_units/@reference_id=='default'/amount/value",
            value: new BigNumber(input.amount).numeric.toString(),
          }
        ]
      })

      return {
        data: {
          ...input.data,
          currency_code: input.currency_code,
        },
      }
    } catch (error: any) {
      throw new MedusaError(
        MedusaError.Types.UNEXPECTED_STATE,
        `Failed to update PayPal payment: ${error.result?.message || error}`
      )
    }
  }
}
```

The `updatePayment` method receives an object with the payment session's `data` field.

In the method, you:

1. Extract the PayPal order ID from the `data` field. This is the same `data.order_id` you returned in the `initiatePayment` method.
2. Update the PayPal order amount.

You return an object with a `data` field containing the updated payment information. Medusa updates the payment session's `data` field with the returned `data` object.

Refer to the [Create Payment Module Provider](https://docs.medusajs.com/references/payment/provider#updatepayment/index.html.md) guide for detailed information about this method.

#### deletePayment Method

The [deletePayment](https://docs.medusajs.com/references/payment/provider#deletepayment/index.html.md) method deletes the payment session in the third-party service. It's called when the customer changes the payment method during checkout.

PayPal orders cannot be deleted, so you can leave this method empty. PayPal will automatically cancel orders that are not approved within a certain timeframe.

Add the following method to the `PayPalPaymentProviderService` class:

```ts title="src/modules/paypal/service.ts"
import type {
  DeletePaymentInput,
  DeletePaymentOutput,
} from "@medusajs/framework/types"

class PayPalPaymentProviderService extends AbstractPaymentProvider<Options> {
  // ...
  async deletePayment(
    input: DeletePaymentInput
  ): Promise<DeletePaymentOutput> {
    // Note: PayPal doesn't have a cancelOrder API endpoint
    // Orders can only be voided if they're authorized, which is handled in cancelPayment
    // For orders that haven't been authorized yet, they will expire automatically

    return {
      data: input.data,
    }
  }
}
```

The `deletePayment` method receives an object with the payment session's `data` field.

In the method, you simply return the existing `data` object without making any changes.

Refer to the [Create Payment Module Provider](https://docs.medusajs.com/references/payment/provider#deletepayment/index.html.md) guide for detailed information about this method.

#### retrievePayment Method

The [retrievePayment](https://docs.medusajs.com/references/payment/provider#retrievepayment/index.html.md) method retrieves the payment details from the third-party service. You'll retrieve the order details from PayPal in this method.

Add the following method to the `PayPalPaymentProviderService` class:

```ts title="src/modules/paypal/service.ts"
import type {
  RetrievePaymentInput,
  RetrievePaymentOutput,
} from "@medusajs/framework/types"

class PayPalPaymentProviderService extends AbstractPaymentProvider<Options> {
  // ...
  async retrievePayment(
    input: RetrievePaymentInput
  ): Promise<RetrievePaymentOutput> {
    try {
      const orderId = input.data?.order_id as string | undefined

      if (!orderId || typeof orderId !== "string") {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          "PayPal order ID is required"
        )
      }

      const response = await this.ordersController_.getOrder({
        id: orderId,
      })

      const order = response.result

      if (!order?.id) {
        throw new MedusaError(
          MedusaError.Types.NOT_FOUND,
          "PayPal order not found"
        )
      }

      return {
        data: {
          order_id: order.id,
          status: order.status,
          intent: order.intent,
        },
      }
    } catch (error: any) {
      throw new MedusaError(
        MedusaError.Types.UNEXPECTED_STATE,
        `Failed to retrieve PayPal payment: ${error.result?.message || error}`
      )
    }
  }
}
```

The `retrievePayment` method receives an object with the payment record's `data` field.

In the method, you:

1. Extract the PayPal order ID from the `data` field. This is the same `data.order_id` you returned in the `initiatePayment` method.
2. Retrieve the PayPal order details.

You return an object with a `data` field containing the retrieved payment information.

Refer to the [Create Payment Module Provider](https://docs.medusajs.com/references/payment/provider#retrievepayment/index.html.md) guide for detailed information about this method.

#### cancelPayment Method

The [cancelPayment](https://docs.medusajs.com/references/payment/provider#cancelpayment/index.html.md) method cancels a previously authorized payment with the third-party service. It's called when an admin user cancels an order from the Medusa Admin dashboard.

You'll void the authorized PayPal payment in this method.

Add the following method to the `PayPalPaymentProviderService` class:

```ts title="src/modules/paypal/service.ts"
import type {
  CancelPaymentInput,
  CancelPaymentOutput,
} from "@medusajs/framework/types"

class PayPalPaymentProviderService extends AbstractPaymentProvider<Options> {
  // ...
  async cancelPayment(
    input: CancelPaymentInput
  ): Promise<CancelPaymentOutput> {
    try {
      const authorizationId = input.data?.authorization_id as string | undefined

      if (!authorizationId || typeof authorizationId !== "string") {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          "PayPal authorization ID is required for cancellation"
        )
      }

      await this.paymentsController_.voidPayment({
        authorizationId: authorizationId,
      })

      return {
        data: input.data,
      }
    } catch (error: any) {
      throw new MedusaError(
        MedusaError.Types.UNEXPECTED_STATE,
        `Failed to cancel PayPal payment: ${error.result?.message || error}`
      )
    }
  }
}
```

The `cancelPayment` method receives an object with the payment record's `data` field.

In the method, you:

1. Extract the PayPal authorization ID from the `data` field. This is the same `data.authorization_id` you returned in the `authorizePayment` method.
2. Void the authorized PayPal payment.

You return an object with the existing `data` field without making any changes. Medusa updates the payment record's `data` field with the returned `data` object.

#### getPaymentStatus Method

The [getPaymentStatus](https://docs.medusajs.com/references/payment/provider#getpaymentstatus/index.html.md) method retrieves the current status of the payment from the third-party service. You'll retrieve the order status from PayPal in this method.

Add the following method to the `PayPalPaymentProviderService` class:

```ts title="src/modules/paypal/service.ts"
import type {
  GetPaymentStatusInput,
  GetPaymentStatusOutput,
} from "@medusajs/framework/types"
import { OrderStatus } from "@paypal/paypal-server-sdk"

class PayPalPaymentProviderService extends AbstractPaymentProvider<Options> {
  // ...
  async getPaymentStatus(
    input: GetPaymentStatusInput
  ): Promise<GetPaymentStatusOutput> {
    try {
      const orderId = input.data?.order_id as string | undefined

      if (!orderId || typeof orderId !== "string") {
        return { status: "pending" as PaymentSessionStatus }
      }

      const response = await this.ordersController_.getOrder({
        id: orderId,
      })

      const order = response.result

      if (!order) {
        return { status: "pending" as PaymentSessionStatus }
      }

      const status = order.status

      switch (status) {
        case OrderStatus.Created:
        case OrderStatus.Saved:
          return { status: "pending" as PaymentSessionStatus }
        case OrderStatus.Approved:
          return { status: "authorized" as PaymentSessionStatus }
        case OrderStatus.Completed:
          return { status: "authorized" as PaymentSessionStatus }
        case OrderStatus.Voided:
          return { status: "canceled" as PaymentSessionStatus }
        default:
          return { status: "pending" as PaymentSessionStatus }
      }
    } catch (error: any) {
      return { status: "pending" as PaymentSessionStatus }
    }
  }
}
```

The `getPaymentStatus` method receives an object with the payment record's `data` field.

In the method, you:

1. Extract the PayPal order ID from the `data` field. This is the same `data.order_id` you returned in the `initiatePayment` method.
2. Retrieve the PayPal order details.
3. Map the PayPal order status to Medusa's `PaymentSessionStatus`.

You return an object with the mapped payment status.

Refer to the [Create Payment Module Provider](https://docs.medusajs.com/references/payment/provider#getpaymentstatus/index.html.md) guide for detailed information about this method.

#### verifyWebhookSignature Method

The `verifyWebhookSignature` method is not required by the `AbstractPaymentProvider` class. You'll create this method to verify PayPal webhook signatures, and use it in the next method that handles webhooks.

Add the following method to the `PayPalPaymentProviderService` class:

```ts title="src/modules/paypal/service.ts"
class PayPalPaymentProviderService extends AbstractPaymentProvider<Options> {
  // ...
  private async verifyWebhookSignature(
    headers: Record<string, any>,
    body: any,
    rawBody: string | Buffer | undefined
  ): Promise<boolean> {
    try {
      if (!this.options_.webhook_id) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          "PayPal webhook ID is required for webhook signature verification"
        )
      }
  
      const transmissionId =
        headers["paypal-transmission-id"]
      const transmissionTime =
        headers["paypal-transmission-time"]
      const certUrl =
        headers["paypal-cert-url"]
      const authAlgo =
        headers["paypal-auth-algo"]
      const transmissionSig =
        headers["paypal-transmission-sig"]
  
      if (
        !transmissionId ||
        !transmissionTime ||
        !certUrl ||
        !authAlgo ||
        !transmissionSig
      ) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          "Missing required PayPal webhook headers"
        )
      }
  
      // PayPal's API endpoint for webhook verification
      const baseUrl =
        this.options_.environment === "production"
          ? "https://api.paypal.com"
          : "https://api.sandbox.paypal.com"
  
      const verifyUrl = `${baseUrl}/v1/notifications/verify-webhook-signature`
  
      // Get access token for verification API call
      const authResponse = await fetch(`${baseUrl}/v1/oauth2/token`, {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
          Authorization: `Basic ${Buffer.from(
            `${this.options_.client_id}:${this.options_.client_secret}`
          ).toString("base64")}`,
        },
        body: "grant_type=client_credentials",
      })
  
      if (!authResponse.ok) {
        throw new MedusaError(
          MedusaError.Types.UNEXPECTED_STATE,
          "Failed to get access token for webhook verification"
        )
      }
  
      const authData = await authResponse.json()
      const accessToken = authData.access_token
  
      if (!accessToken) {
        throw new MedusaError(
          MedusaError.Types.UNEXPECTED_STATE,
          "Access token not received from PayPal"
        )
      }
  
      let webhookEvent: any
      if (rawBody) {
        const rawBodyString =
          typeof rawBody === "string" ? rawBody : rawBody.toString("utf8")
        try {
          webhookEvent = JSON.parse(rawBodyString)
        } catch (e) {
          this.logger_.warn("Raw body is not valid JSON, using parsed body")
          webhookEvent = body
        }
      } else {
        this.logger_.warn(
          "Raw body not available, using parsed body. Verification may fail if formatting differs."
        )
        webhookEvent = body
      }
  
      const verifyPayload = {
        transmission_id: transmissionId,
        transmission_time: transmissionTime,
        cert_url: certUrl,
        auth_algo: authAlgo,
        transmission_sig: transmissionSig,
        webhook_id: this.options_.webhook_id,
        webhook_event: webhookEvent,
      }
  
      const verifyResponse = await fetch(verifyUrl, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${accessToken}`,
        },
        body: JSON.stringify(verifyPayload),
      })
  
      if (!verifyResponse.ok) {
        throw new MedusaError(
          MedusaError.Types.UNEXPECTED_STATE,
          "Webhook verification API call failed"
        )
      }
  
      const verifyData = await verifyResponse.json()
  
      // PayPal returns verification_status: "SUCCESS" if verification passes
      const isValid = verifyData.verification_status === "SUCCESS"
  
      if (!isValid) {
        throw new MedusaError(
          MedusaError.Types.UNEXPECTED_STATE,
          "Webhook signature verification failed"
        )
      }
  
      return isValid
    } catch (e) {
      this.logger_.error("PayPal verifyWebhookSignature error:", e)
      return false
    }
  }
}
```

The `verifyWebhookSignature` method receives the following parameters:

1. `headers`: The HTTP headers from the webhook request.
2. `body`: The parsed JSON body from the webhook request.
3. `rawBody`: The raw body from the webhook request as a string or buffer.

In the method, you:

1. Extract the required PayPal webhook headers.
2. Get an [access token](https://developer.paypal.com/api/rest/authentication/) for the PayPal webhook verification API.
3. Construct the verification payload with the extracted headers and webhook event data.
4. Call the PayPal [webhook verification API](https://developer.paypal.com/api/rest/webhooks/rest/#link-postbackmethod) to verify the webhook signature.

You return `true` if the webhook signature is valid (based on the API's response), or `false` otherwise.

#### getWebhookActionAndData Method

The [getWebhookActionAndData](https://docs.medusajs.com/references/payment/provider#getwebhookactionanddata/index.html.md) method processes incoming webhook events from the third-party service. Medusa provides a webhook endpoint at `/hooks/payment/{provider_id}` that you can use to receive PayPal webhooks. This endpoint calls the `getWebhookActionAndData` method to process the webhook event.

Add the following method to the `PayPalPaymentProviderService` class:

```ts title="src/modules/paypal/service.ts"
import { PaymentActions } from "@medusajs/framework/utils"
import type {
  ProviderWebhookPayload,
  WebhookActionResult,
} from "@medusajs/framework/types"

class PayPalPaymentProviderService extends AbstractPaymentProvider<Options> {
  // ...
  async getWebhookActionAndData(
    payload: ProviderWebhookPayload["payload"]
  ): Promise<WebhookActionResult> {
    try {
      const { data, rawData, headers } = payload

      // Verify webhook signature
      const isValid = await this.verifyWebhookSignature(
        headers || {},
        data,
        rawData || ""
      )

      if (!isValid) {
        this.logger_.error("Invalid PayPal webhook signature")
        return {
          action: "failed",
          data: {
            session_id: "",
            amount: new BigNumber(0),
          },
        }
      }

      // PayPal webhook events have event_type
      const eventType = (data as any)?.event_type

      if (!eventType) {
        this.logger_.warn("PayPal webhook event missing event_type")
        return {
          action: "not_supported",
          data: {
            session_id: "",
            amount: new BigNumber(0),
          },
        }
      }

      // Extract order ID and amount from webhook payload
      const resource = (data as any)?.resource
      let sessionId: string | undefined = (data as any)?.resource?.custom_id

      if (!sessionId) {
        this.logger_.warn("Session ID not found in PayPal webhook resource")
        return {
          action: "not_supported",
          data: {
            session_id: "",
            amount: new BigNumber(0),
          },
        }
      }

      const amountValue =
        resource?.amount?.value ||
        resource?.purchase_units?.[0]?.payments?.captures?.[0]?.amount
          ?.value ||
        resource?.purchase_units?.[0]?.payments?.authorizations?.[0]
          ?.amount?.value ||
        0

      const amount = new BigNumber(amountValue)
      const payloadData = {
        session_id: sessionId,
        amount,
      }

      // Map PayPal webhook events to Medusa actions
      switch (eventType) {
        case "PAYMENT.AUTHORIZATION.CREATED":
          return {
            action: PaymentActions.AUTHORIZED,
            data: payloadData,
          }

        case "PAYMENT.CAPTURE.DENIED":
          return {
            action: PaymentActions.FAILED,
            data: payloadData,
          }

        case "PAYMENT.AUTHORIZATION.VOIDED":
          return {
            action: PaymentActions.CANCELED,
            data: payloadData,
          }
        
        case "PAYMENT.CAPTURE.COMPLETED":
          return {
            action: PaymentActions.SUCCESSFUL,
            data: payloadData,
          }

        default:
          this.logger_.info(`Unhandled PayPal webhook event: ${eventType}`)
          return {
            action: PaymentActions.NOT_SUPPORTED,
            data: payloadData,
          }
      }
    } catch (error: any) {
      this.logger_.error("PayPal getWebhookActionAndData error:", error.result?.message || error)
      return {
        action: "failed",
        data: {
          session_id: "",
          amount: new BigNumber(0),
        },
      }
    }
  }
}
```

The `getWebhookActionAndData` method receives a `payload` object containing the webhook request data.

In the method, you:

1. Verify the webhook signature using the `verifyWebhookSignature` method.
2. Extract the `event_type` from the webhook payload to determine the type of event.
3. Extract the session ID and amount from the webhook resource.
4. Map the PayPal webhook event types to Medusa actions.

You return an object containing the action Medusa should take (such as `authorized`), along with the payment session ID and amount. Based on the returned action, Medusa uses the methods you implemented earlier to perform the necessary operations.

### d. Export Module Definition

You've now finished implementing the necessary methods for the PayPal Payment Module Provider.

The final piece to a module is its definition, which you export in an `index.ts` file at the module's root directory. This definition tells Medusa the module's details, including its service.

To create the module's definition, create the file `src/modules/paypal/index.ts` with the following content:

```ts title="src/modules/paypal/index.ts"
import PayPalPaymentProviderService from "./service"
import { ModuleProvider, Modules } from "@medusajs/framework/utils"

export default ModuleProvider(Modules.PAYMENT, {
  services: [PayPalPaymentProviderService],
})
```

You use `ModuleProvider` from the Modules SDK to create the module provider's definition. It accepts two parameters:

1. The name of the module that this provider belongs to, which is `Modules.PAYMENT` in this case.
2. An object with a required property `services` indicating the Module Provider's services.

### e. Add Module Provider to Medusa's Configuration

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    // ...
    {
      resolve: "@medusajs/medusa/payment",
      options: {
        providers: [
          {
            resolve: "./src/modules/paypal",
            id: "paypal",
            options: {
              client_id: process.env.PAYPAL_CLIENT_ID!,
              client_secret: process.env.PAYPAL_CLIENT_SECRET!,
              environment: process.env.PAYPAL_ENVIRONMENT || "sandbox",
              autoCapture: process.env.PAYPAL_AUTO_CAPTURE === "true",
              webhook_id: process.env.PAYPAL_WEBHOOK_ID,
            },
          },
        ],
      },
    },
  ],
})
```

To pass Payment Module Providers to the Payment Module, add the `modules` property to the Medusa configuration and pass the Payment Module in its value.

The Payment Module accepts a `providers` option, which is an array of Payment Module Providers to register.

To register the PayPal Payment Module Provider, you add an object to the `providers` array with the following properties:

- `resolve`: The NPM package or path to the module provider. In this case, it's the path to the `src/modules/paypal` directory.
- `id`: The ID of the module provider. The Payment Module Provider is then registered with the ID `pp_{identifier}_{id}`, where:
  - `{identifier}`: The identifier static property defined in the Module Provider's service, which is `paypal` in this case.
  - `{id}`: The ID set in this configuration, which is also `paypal` in this case.
- `options`: The options to pass to the module provider. These are the options you defined in the `Options` interface of the module provider's service.

### f. Set Options as Environment Variables

Next, you'll set the necessary options as environment variables. You'll retrieve their values from the [PayPal Developer Dashboard](https://developer.paypal.com/dashboard/).

#### PayPal Client ID and Secret

To get your PayPal Client ID and Secret:

1. [Log in to the PayPal Developer Dashboard](https://developer.paypal.com/dashboard/).
2. Make sure you're in the correct environment (Sandbox or Live) using the environment toggle at the top left. It's recommended to use Sandbox for development and testing.

![PayPal environment toggle highlighted with Sandbox selected](https://res.cloudinary.com/dza7lstvk/image/upload/v1765207135/Medusa%20Resources/CleanShot_2025-12-08_at_17.18.06_2x_sbm8bv.png)

2. Go to **Apps & Credentials**.
3. If you don't have a default app, create one by clicking **Create App**.
   - Enter app name, set type to "Merchant", and select the sandbox business account.
4. Click on your app to view its details.
5. Copy the **Client ID** and **Secret** values.

![PayPal app details showing Client ID and Secret](https://res.cloudinary.com/dza7lstvk/image/upload/v1765207279/Medusa%20Resources/CleanShot_2025-12-08_at_17.20.49_2x_zu9muk.png)

Then, set these values as environment variables in your `.env` file:

```shell title=".env"
PAYPAL_CLIENT_ID=your_paypal_client_id
PAYPAL_CLIENT_SECRET=your_paypal_client_secret
```

#### PayPal Environment and Auto-Capture Option

Next, you can set the following optional environment variables in your `.env` file:

```shell title=".env"
PAYPAL_ENVIRONMENT=sandbox # or "production" for live
PAYPAL_AUTO_CAPTURE=true # or "false" to authorize only
```

Where:

- `PAYPAL_ENVIRONMENT`: The PayPal environment to use, either `sandbox` for testing or `production` for live transactions. Default is `sandbox`.
- `PAYPAL_AUTO_CAPTURE`: Whether to capture payments immediately (`true`) or authorize only (`false`). Default is `false`.

#### PayPal Webhook ID

Finally, you'll set up a webhook in the PayPal Developer Dashboard to receive payment events. Webhooks require a publicly accessible URL. In this section, you'll use [ngrok](https://ngrok.com/) to create a temporary public URL for testing webhooks locally.

Deploy your Medusa application with [Cloud](https://docs.medusajs.com/cloud/sign-up/index.html.md) in minutes. Benefit from features like zero-configuration deployments, automatic scaling, GitHub integration, and more.

To set up ngrok and create a public URL, run the following command in your terminal:

```shell
npx ngrok http 9000
```

This will create a public URL that tunnels to your local Medusa server running on port `9000`. Copy the generated URL (for example, `https://abcd1234.ngrok.io`).

Then, on the [PayPal Developer Dashboard](https://developer.paypal.com/dashboard/):

1. Go to **Apps & Credentials**.
2. Click on your app to view its details.
3. Scroll down to the **Webhooks** (or **Sandbox Webhooks**) section and click **Add Webhook**.
4. In the **Webhook URL** field, enter your ngrok URL followed by `/hooks/payment/paypal_paypal`. For example: `https://abcd1234.ngrok.io/hooks/payment/paypal_paypal`.
   - The URL format is `{base_url}/hooks/payment/{provider_id}`, where `provider_id` is `paypal_paypal` (the combination of the `identifier` and `id` from your configuration).
5. In the **Event Types** section, select the following events:
   - Payment authorization created
   - Payment authorization voided
   - Payment capture completed
   - Payment capture denied
6. Click **Save** to create the webhook.

Then, copy the **Webhook ID** from the webhook details. Set it as an environment variable in your `.env` file:

```shell title=".env"
PAYPAL_WEBHOOK_ID=your_paypal_webhook_id
```

Make sure the ngrok command remains running while you test PayPal webhooks locally. If you restart ngrok, you'll get a new public URL, and you'll need to update the webhook URL in the PayPal Developer Dashboard accordingly.

In the next steps, you'll customize the Next.js Starter Storefront to support paying with PayPal, then test out the integration.

***

## Step 3: Enable PayPal Module Provider

In this step, you'll enable the PayPal Payment Module Provider in a region of your Medusa store. A region is a geographical area where you sell products, and each region has its own settings, such as currency and payment providers.

You must enable the PayPal Payment Module Provider in at least one region. To do this:

1. Run the following command to start your Medusa application:

```bash npm2yarn
npm run dev
```

2. Open the Medusa Admin dashboard in your browser at `http://localhost:9000/app` and log in.
3. Go to Settings -> Regions.
4. Click on the <InlineIcon Icon={EllipsisHorizontal} alt="three-dots" /> icon next to the region you want to enable PayPal for, then click **Edit**.
5. In the **Payment Providers** dropdown, select **PayPal (PAYPAL)** to add it to the region.
6. Click **Save** to update the region.

Repeat these steps for every region where you want to enable the PayPal Payment Module Provider.

![Medusa Admin dashboard showing region edit screen with PayPal selected as payment provider](https://res.cloudinary.com/dza7lstvk/image/upload/v1765265899/Medusa%20Resources/CleanShot_2025-12-09_at_09.36.54_2x_g4guz6.png)

***

## Step 4: Add PayPal to Storefront

In this step, you'll customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) that you set up with the Medusa application to support paying with PayPal. You'll add a PayPal button to the checkout page that allows customers to pay using PayPal.

The Next.js Starter Storefront was installed in a separate directory from your Medusa application. The storefront directory's name follows the pattern `{your-project}-storefront`.

For example, if your Medusa application's directory is `medusa-paypal`, you can find the storefront by going to the parent directory and changing to `medusa-paypal-storefront`:

```bash
cd ../medusa-paypal-storefront # change based on your project name
```

### a. Install PayPal SDK

To add the PayPal button, you'll use the [PayPal React SDK](https://www.npmjs.com/package/@paypal/react-paypal-js). This SDK provides React components that make it easy to integrate PayPal into your React application.

In the storefront directory, run the following command to install the PayPal React SDK:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm install @paypal/react-paypal-js
```

### b. Add Client ID Environment Variable

Next, add the PayPal Client ID as an environment variable in the storefront.

Copy the same PayPal Client ID you used in the Medusa application, then add it to the `.env.local` file in the storefront directory:

```shell title=".env.local" badgeLabel="Storefront" badgeColor="blue"
NEXT_PUBLIC_PAYPAL_CLIENT_ID=your_paypal_client_id
```

### c. Add PayPal Wrapper Component

Next, you'll add a PayPal wrapper component that initializes the PayPal SDK and provides the PayPal context to its child components.

Create the file `src/modules/checkout/components/payment-wrapper/paypal-wrapper.tsx` with the following content:

```tsx title="src/modules/checkout/components/payment-wrapper/paypal-wrapper.tsx" badgeLabel="Storefront" badgeColor="blue"
"use client"

import { PayPalScriptProvider } from "@paypal/react-paypal-js"
import { HttpTypes } from "@medusajs/types"
import { createContext } from "react"

type PayPalWrapperProps = {
  paymentSession: HttpTypes.StorePaymentSession
  children: React.ReactNode
}

export const PayPalContext = createContext(false)

const PayPalWrapper: React.FC<PayPalWrapperProps> = ({
  paymentSession,
  children,
}) => {
  const clientId = process.env.NEXT_PUBLIC_PAYPAL_CLIENT_ID

  if (!clientId) {
    throw new Error(
      "PayPal client ID is missing. Set NEXT_PUBLIC_PAYPAL_CLIENT_ID environment variable or ensure payment session has client_id."
    )
  }

  const initialOptions = {
    clientId,
    currency: paymentSession.currency_code.toUpperCase() || "USD",
    intent: paymentSession.data?.intent === "CAPTURE" ? "capture" : "authorize",
  }

  return (
    <PayPalContext.Provider value={true}>
      <PayPalScriptProvider options={initialOptions}>
        {children}
      </PayPalScriptProvider>
    </PayPalContext.Provider>
  )
}

export default PayPalWrapper
```

You create a `PayPalWrapper` component that accepts a Medusa payment session and children components as props.

In the component, you:

1. Retrieve the PayPal Client ID from the environment variable `NEXT_PUBLIC_PAYPAL_CLIENT_ID`.
2. Set the initial options for the PayPal SDK, including the client ID, currency, and intent (capture or authorize).
   - You set the intent based on the `data.intent` field in the payment session. You set this field in the `initiatePayment` method of the PayPal Payment Module Provider's service. Its value depends on the `autoCapture` option.
3. Wrap the children components with the `PayPalScriptProvider` component from the PayPal React SDK, passing the initial options.

Next, you'll use this wrapper component in the checkout page to provide the PayPal context to the PayPal button component you'll add later.

In `src/modules/checkout/components/payment-wrapper/index.tsx`, add the following imports at the top of the file:

```tsx title="src/modules/checkout/components/payment-wrapper/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import PayPalWrapper from "./paypal-wrapper"
import { isPaypal } from "@lib/constants"
```

Then, in the `PaymentWrapper` component, add the following before the last `return` statement:

```tsx title="src/modules/checkout/components/payment-wrapper/index.tsx" badgeLabel="Storefront" badgeColor="blue"
if (isPaypal(paymentSession?.provider_id) && paymentSession) {
  return (
    <PayPalWrapper
      paymentSession={paymentSession}
    >
      {children}
    </PayPalWrapper>
  )
}
```

If the customer has selected PayPal as the payment method, you wrap the children components with the `PayPalWrapper` component, passing the payment session as a prop.

### d. Add PayPal Button Component

Next, you'll add a PayPal button component that renders the PayPal button and handles the payment process.

Create the file `src/modules/checkout/components/payment-button/paypal-payment-button.tsx` with the following content:

```tsx title="src/modules/checkout/components/payment-button/paypal-payment-button.tsx" badgeLabel="Storefront" badgeColor="blue"
"use client"

import { PayPalButtons, usePayPalScriptReducer } from "@paypal/react-paypal-js"
import { placeOrder } from "@lib/data/cart"
import { HttpTypes } from "@medusajs/types"
import { Button } from "@medusajs/ui"
import React, { useState } from "react"
import ErrorMessage from "../error-message"

type PayPalPaymentButtonProps = {
  cart: HttpTypes.StoreCart
  notReady: boolean
  "data-testid"?: string
}

const PayPalPaymentButton: React.FC<PayPalPaymentButtonProps> = ({
  cart,
  notReady,
  "data-testid": dataTestId,
}) => {
  const [submitting, setSubmitting] = useState(false)
  const [errorMessage, setErrorMessage] = useState<string | null>(null)
  const [{ isResolved }] = usePayPalScriptReducer()

  const paymentSession = cart.payment_collection?.payment_sessions?.find(
    (s) => s.status === "pending"
  )

  // TODO: add function handlers
}

export default PayPalPaymentButton
```

You create a `PayPalPaymentButton` component that accepts the cart, a `notReady` flag, and an optional `data-testid` prop for testing.

In the component, you initialize the following variables:

- `submitting`: A state variable to track if the payment is being submitted.
- `errorMessage`: A state variable to store any error messages.
- `isResolved`: A variable from the PayPal SDK that indicates whether the PayPal SDK script has loaded.
- `paymentSession`: The pending PayPal payment session from the cart's payment collection.

Next, you'll add the function handlers for creating PayPal orders and placing the Medusa order.

Replace the `// TODO: add function handlers` comment with the following:

```tsx title="src/modules/checkout/components/payment-button/paypal-payment-button.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={functionHandlerHighlights}
const onPaymentCompleted = async () => {
  await placeOrder()
    .catch((err) => {
      setErrorMessage(err.message)
    })
    .finally(() => {
      setSubmitting(false)
    })
}

// Get PayPal order ID from payment session data
// The Medusa PayPal provider should create a PayPal order during initialization
// and store the order ID in the payment session data
const getPayPalOrderId = (): string | null => {
  if (!paymentSession?.data) {
    return null
  }

  // Try different possible keys where the order ID might be stored
  return (
    (paymentSession.data.order_id as string) ||
    (paymentSession.data.orderId as string) ||
    (paymentSession.data.id as string) ||
    null
  )
}

const createOrder = async () => {
  setSubmitting(true)
  setErrorMessage(null)

  try {
    if (!paymentSession) {
      throw new Error("Payment session not found")
    }

    // Check if Medusa server already created a PayPal order
    const existingOrderId = getPayPalOrderId()

    if (existingOrderId) {
      // Medusa already created the order, use that order ID
      return existingOrderId
    }

    // If no order ID exists, we need to create one
    // This might happen if the PayPal provider doesn't create orders during initialization
    // In this case, we'll need to create the order via PayPal API
    // For now, throw an error - the backend should handle order creation
    throw new Error(
      "PayPal order not found. Please ensure the payment session is properly initialized."
    )
  } catch (error: any) {
    setErrorMessage(error.message || "Failed to create PayPal order")
    setSubmitting(false)
    throw error
  }
}

const onApprove = async (data: { orderID: string }) => {
  try {
    setSubmitting(true)
    setErrorMessage(null)

    // After PayPal approval, place the order
    // The Medusa server will handle the payment authorization
    await onPaymentCompleted()
  } catch (error: any) {
    setErrorMessage(error.message || "Failed to process PayPal payment")
    setSubmitting(false)
  }
}

const onError = (err: Record<string, unknown>) => {
  setErrorMessage(
    (err.message as string) || "An error occurred with PayPal payment"
  )
  setSubmitting(false)
}

const onCancel = () => {
  setSubmitting(false)
  setErrorMessage("PayPal payment was cancelled")
}

// TODO: add a return statement
```

You add the following function handlers:

- `onPaymentCompleted`: Places the Medusa order by calling the `placeOrder` function. This function is called after the PayPal payment is approved.
- `getPayPalOrderId`: Retrieves the PayPal order ID from the payment session's `data` field.
- `createOrder`: Returns the PayPal order ID that was created by the Medusa server during payment initialization. If no order ID exists, it throws an error.
- `onApprove`: Called when the customer approves the PayPal payment. It calls the `onPaymentCompleted` function to place the Medusa order.
- `onError`: Called when an error occurs during the PayPal payment process. It updates the error message state.
- `onCancel`: Called when the customer cancels the PayPal payment. It updates the error message state.

Finally, you'll add the return statement to render the PayPal button.

Replace the `// TODO: add a return statement` comment with the following:

```tsx title="src/modules/checkout/components/payment-button/paypal-payment-button.tsx" badgeLabel="Storefront" badgeColor="blue"
// If PayPal SDK is not ready, show a loading button
if (!isResolved) {
  return (
    <>
      <Button
        disabled={true}
        size="large"
        isLoading={true}
        data-testid={dataTestId}
      >
        Loading PayPal...
      </Button>
      <ErrorMessage
        error={errorMessage}
        data-testid="paypal-payment-error-message"
      />
    </>
  )
}

return (
  <>
    <div className="mb-4">
      <PayPalButtons
        createOrder={createOrder}
        onApprove={onApprove}
        onError={onError}
        onCancel={onCancel}
        style={{
          layout: "horizontal",
          color: "black",
          shape: "rect",
          label: "buynow",
        }}
        disabled={notReady || submitting}
      />
    </div>
    <ErrorMessage
      error={errorMessage}
      data-testid="paypal-payment-error-message"
    />
  </>
)
```

You render two different states:

- If the PayPal SDK is not ready, you render a loading button.
- If the SDK is ready, you render the `PayPalButtons` component from the PayPal React SDK, passing the function handlers as props.

### e. Use PayPal Button in Checkout Page

Next, you'll use the `PayPalPaymentButton` component in the checkout page to allow customers to pay with PayPal.

In `src/modules/checkout/components/payment-button/index.tsx`, add the following imports at the top of the file:

```tsx title="src/modules/checkout/components/payment-button/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { isPaypal } from "@lib/constants"
import PayPalPaymentButton from "./paypal-payment-button"
```

Then, in the `PaymentButton` component, add to the `switch` block a case for PayPal:

```tsx title="src/modules/checkout/components/payment-button/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const PaymentButton: React.FC<PaymentButtonProps> = ({
  cart,
  "data-testid": dataTestId,
}) => {
  // ...
  switch (true) {
    case isPaypal(paymentSession?.provider_id):
      return (
        <PayPalPaymentButton
          notReady={notReady}
          cart={cart}
          data-testid={dataTestId}
        />
      )
    // ...
  }
}
```

When the customer selects PayPal as the payment method, you render the `PayPalPaymentButton` component, passing the cart and `notReady` flag as props.

### f. Handle Selecting PayPal in Checkout Page

Finally, you'll handle selecting PayPal as the payment method in the checkout page. You'll ensure that when the customer selects PayPal, the payment session is created and initialized correctly.

In `src/modules/checkout/components/payment/index.tsx`, add the following import at the top of the file:

```tsx title="src/modules/checkout/components/payment/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { isPaypal } from "@lib/constants"
```

Then, replace the `setPaymentMethod` function defined in the `Payment` component with the following:

```tsx title="src/modules/checkout/components/payment/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={setPaymentMethodHighlights}
const setPaymentMethod = async (method: string) => {
  setError(null)
  setSelectedPaymentMethod(method)
  if (isStripeLike(method) || isPaypal(method)) {
    await initiatePaymentSession(cart, {
      provider_id: method,
    })
  }
}
```

You change the `if` condition in the `setPaymentMethod` function to also initialize the payment session when PayPal is selected.

Finally, change the `handleSubmit` function defined in the `Payment` component to the following:

```tsx title="src/modules/checkout/components/payment/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={handleSubmitHighlights}
const handleSubmit = async () => {
  setIsLoading(true)
  try {
    const shouldInputCard =
      isStripeLike(selectedPaymentMethod) && !activeSession

    const checkActiveSession =
      activeSession?.provider_id === selectedPaymentMethod

    if (!checkActiveSession) {
      await initiatePaymentSession(cart, {
        provider_id: selectedPaymentMethod,
      })
    }

    // For PayPal, we don't need to input card details, so go to review
    if (!shouldInputCard || isPaypal(selectedPaymentMethod)) {
      return router.push(
        pathname + "?" + createQueryString("step", "review"),
        {
          scroll: false,
        }
      )
    }
  } catch (err: any) {
    setError(err.message)
  } finally {
    setIsLoading(false)
  }
}
```

You mainly change the condition that checks whether to navigate to the review step. If PayPal is selected, you navigate to the review step directly since no card details are needed.

### Test the PayPal Integration

You can now test the PayPal integration by placing an order from the Next.js Starter Storefront.

#### Get Sandbox PayPal Account Credentials

Before you test the integration, you'll need to get sandbox PayPal account credentials to use for testing payments.

To get sandbox PayPal account credentials:

1. [Go to your PayPal Developer Dashboard](https://developer.paypal.com/dashboard/).
2. Make sure you're in the Sandbox environment using the environment toggle at the top left.
3. Go to **Testing Tools** -> **Sandbox Accounts**.
4. Click on the email ending with `@personal.example.com` to view the account details.

![PayPal Developer Dashboard showing sandbox accounts](https://res.cloudinary.com/dza7lstvk/image/upload/v1765267446/Medusa%20Resources/CleanShot_2025-12-09_at_10.03.49_2x_phd4cj.png)

5. On the account details page, copy the **Email** and **Password** values. You'll use those to pay with PayPal during testing.

#### Test Checkout with PayPal

First, run the Medusa application with the following command:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

Then, run the Next.js Starter Storefront with the following command in the storefront directory:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

Open the storefront at `http://localhost:8000` in your browser. Add an item to the cart and proceed to checkout.

On the payment step, select PayPal as the payment method, then click **Continue to Review**.

![Next.js Starter Storefront checkout page showing PayPal selected as payment method](https://res.cloudinary.com/dza7lstvk/image/upload/v1765267123/Medusa%20Resources/CleanShot_2025-12-09_at_09.58.25_2x_b4ogqz.png)

This navigates you to the review step, where a PayPal button appears for completing your order.

![Next.js Starter Storefront checkout page showing PayPal button](https://res.cloudinary.com/dza7lstvk/image/upload/v1765267289/Medusa%20Resources/CleanShot_2025-12-09_at_09.59.36_2x_r9sboh.png)

Click the PayPal button to be redirected to PayPal's payment page. On the PayPal login page, use the [sandbox account credentials](#get-sandbox-paypal-account-credentials) you obtained earlier to log in and complete the payment.

Once you complete the payment, PayPal redirects you back to the storefront's order confirmation page.

#### Check Webhook Event Handling

If you've [set up webhooks](#paypal-webhook-id) using ngrok or with your deployed Medusa instance, PayPal sends webhook events to your Medusa application after payment completion.

You'll see the following logged in your Medusa application's terminal:

```bash
info:    Processing payment.webhook_received which has 1 subscribers
http:    POST /hooks/payment/paypal_paypal ← - (200) - 6.028 ms
```

Medusa uses the `getWebhookActionAndData` method you implemented earlier to process the webhook event and perform any necessary actions, such as authorizing the payment.

#### Capturing and Refunding Payments

In the Medusa Admin dashboard, you can go to Orders and view the order you just placed. You can see the payment status and details.

From the order's details page, you can capture the authorized payment, and refund the captured payment from the **Payments** section. Medusa will use your PayPal Payment Module Provider to perform these actions.

![Medusa Admin dashboard showing order payment details with capture and refund options](https://res.cloudinary.com/dza7lstvk/image/upload/v1765267658/Medusa%20Resources/CleanShot_2025-12-09_at_10.06.57_2x_qsazep.png)

***

## Next Steps

You've successfully integrated PayPal with Medusa. You can now receive payments using PayPal in your Medusa store.

### Learn More About Medusa

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.


# Integrate Medusa with Resend (Email Notifications)

In this guide, you'll learn how to integrate Medusa with Resend.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. Medusa's architecture supports integrating third-party services, such as an email service, that allow you to build your unique requirements around core commerce flows.

[Resend](https://resend.com/docs/introduction) is an email service with an intuitive developer experience to send emails from any application type, including Node.js servers. By integrating Resend with Medusa, you can build flows to send an email when a commerce operation is performed, such as when an order is placed.

This guide will teach you how to:

- Install and set up Medusa.
- Integrate Resend into Medusa for sending emails.
- Build a flow to send an email with Resend when a customer places an order.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer.

[Example Repository](https://github.com/medusajs/examples/tree/main/resend-integration): Find the full code of the guide in this repository.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when you're asked whether you want to install the Next.js Starter Storefront, choose `Y` for yes.

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credential and submit the form. Afterwards, you can login with the new user and explore the dashboard.

The Next.js Starter Storefront is also running at `http://localhost:8000`.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Prepare Resend Account

If you don't have a Resend Account, create one on [their website](https://resend.com/emails).

In addition, Resend allows you to send emails from the address `onboarding@resend.dev` only to your account's email, which is useful for development purposes. If you have a custom domain to send emails from, add it to your Resend account's domains:

1. Go to Domains from the sidebar.
2. Click on Add Domain.

![Resend dashboard interface showing the Domains section in the left sidebar navigation and the prominent "Add Domain" button in the main content area, which initiates the process of configuring a custom domain for email sending through the Resend service](https://res.cloudinary.com/dza7lstvk/image/upload/v1732523238/Medusa%20Resources/Screenshot_2024-11-25_at_10.18.11_AM_pmqgtv.png)

3\. In the form that opens, enter your domain name and select a region close to your users, then click Add.

![Domain configuration modal in Resend showing form fields for entering the custom domain name and selecting the appropriate geographical region for optimal email delivery performance, with an "Add" button to confirm the domain setup](https://res.cloudinary.com/dza7lstvk/image/upload/v1732523280/Medusa%20Resources/Screenshot_2024-11-25_at_10.18.52_AM_sw2pr4.png)

4\. In the domain's details page that opens, you'll find DNS records to add to your DNS provider. After you add them, click on Verify DNS Records. You can start sending emails from your custom domain once it's verified.

The steps to add DNS records are different for each provider, so refer to your provider's documentation or knowledge base for more details.

![The DNS records to add are in a table under the DNS Records section. Once added, click the Verify DNS Records button at the top right.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732523394/Medusa%20Resources/Screenshot_2024-11-25_at_10.20.56_AM_ktvbse.png)

You also need an API key to connect to your Resend account from Medusa, but you'll create that one in a later section.

***

## Step 3: Install Resend Dependencies

In this step, you'll install two packages useful for your Resend integration:

1. `resend`, which is the Resend SDK:

```bash npm2yarn
npm install resend
```

2\. [react-email](https://github.com/resend/react-email),  which is a package created by Resend to create email templates with React:

```bash npm2yarn
npm install @react-email/components -E
```

You'll use these packages in the next steps.

***

## Step 4: Create Resend Module Provider

To integrate third-party services into Medusa, you create a custom module. A module is a re-usable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

Medusa's Notification Module delegates sending notifications to other modules, called module providers. In this step, you'll create a Resend Module Provider that implements sending notifications through the email channel. In later steps, you'll send email notifications with Resend when an order is placed through this provider.

Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).

### Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/resend`.

### Create Service

You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, which is useful if your module defines tables in the database, or connect to a third-party service.

In this section, you'll create the Resend Module Provider's service and the methods necessary to send an email with Resend.

Start by creating the file `src/modules/resend/service.ts` with the following content:

```ts title="src/modules/resend/service.ts" highlights={serviceHighlights1}
import { 
  AbstractNotificationProviderService,
} from "@medusajs/framework/utils"
import { 
  Logger,
} from "@medusajs/framework/types"
import { 
  Resend,
} from "resend"

type ResendOptions = {
  api_key: string
  from: string
  html_templates?: Record<string, {
    subject?: string
    content: string
  }>
}

class ResendNotificationProviderService extends AbstractNotificationProviderService {
  static identifier = "notification-resend"
  private resendClient: Resend
  private options: ResendOptions
  private logger: Logger

  // ...
}

export default ResendNotificationProviderService
```

A Notification Module Provider's service must extend the `AbstractNotificationProviderService`. It has a `send` method that you'll implement soon. The service must also have an `identifier` static property, which is a unique identifier that the Medusa application will use to register the provider in the database.

The `ResendNotificationProviderService` class also has the following properties:

- `resendClient` of type `Resend` (from the Resend SDK you installed in the previous step) to send emails through Resend.
- `options` of type `ResendOptions`. Modules accept options through Medusa's configurations. This ensures that the module is reusable across applications and you don't use sensitive variables like API keys directly in your code. The options that the Resend Module Provider accepts are:
  - `api_key`: The Resend API key.
  - `from`: The email address to send the emails from.
  - `html_templates`: An optional object to replace the default subject and template that the Resend Module uses. This is also useful to support custom emails in different Medusa application setups.
- `logger` property, which is an instance of Medusa's [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md), to log messages.

To send requests using the `resendClient`, you need to initialize it in the class's constructor. So, add the following constructor to `ResendNotificationProviderService`:

```ts title="src/modules/resend/service.ts"
// ...

type InjectedDependencies = {
  logger: Logger
}

class ResendNotificationProviderService extends AbstractNotificationProviderService {
  // ...
  constructor(
    { logger }: InjectedDependencies, 
    options: ResendOptions
  ) {
    super()
    this.resendClient = new Resend(options.api_key)
    this.options = options
    this.logger = logger
  }
}
```

A module's service accepts two parameters:

1. Dependencies resolved from the [Module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md), which is the module's local registry that the Medusa application adds Framework tools to. In this service, you resolve the [Logger utility](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) from the module's container.
2. The module's options that are passed to the module in Medusa's configuration as you'll see in a later section.

Using the API key passed in the module's options, you initialize the Resend client. You also set the `options` and `logger` properties.

#### Validate Options Method

A Notification Module Provider's service can implement a static `validateOptions` method that ensures the options passed to the module through Medusa's configurations are valid.

So, add to the `ResendNotificationProviderService` the `validateOptions` method:

```ts title="src/modules/resend/service.ts"
// other imports...
import { 
  // other imports...
  MedusaError,
} from "@medusajs/framework/utils"

// ...

class ResendNotificationProviderService extends AbstractNotificationProviderService {
  // ...
  static validateOptions(options: Record<any, any>) {
    if (!options.api_key) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Option `api_key` is required in the provider's options."
      )
    }
    if (!options.from) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Option `from` is required in the provider's options."
      )
    }
  }
}
```

In the `validateOptions` method, you throw an error if the `api_key` or `from` options aren't passed to the module. To throw errors, you use `MedusaError` from the Modules SDK. This ensures errors follow Medusa's conventions and are displayed similar to Medusa's errors.

#### Implement Template Methods

Each email type has a different template and content. For example, order confirmation emails show the order's details, whereas customer confirmation emails show a greeting message to the customer.

So, add two methods to the `ResendNotificationProviderService` class that retrieve the email template and subject of a specified template type:

```ts title="src/modules/resend/service.ts" highlights={serviceHighlights2}
// imports and types...

enum Templates {
  ORDER_PLACED = "order-placed",
}

const templates: {[key in Templates]?: (props: unknown) => React.ReactNode} = {
  // TODO add templates
}

class ResendNotificationProviderService extends AbstractNotificationProviderService {
  // ...
  getTemplate(template: Templates) {
    if (this.options.html_templates?.[template]) {
      return this.options.html_templates[template].content
    }
    const allowedTemplates = Object.keys(templates)

    if (!allowedTemplates.includes(template)) {
      return null
    }

    return templates[template]
  }

  getTemplateSubject(template: Templates) {
    if (this.options.html_templates?.[template]?.subject) {
      return this.options.html_templates[template].subject
    }
    switch(template) {
      case Templates.ORDER_PLACED:
        return "Order Confirmation"
      default:
        return "New Email"
    }
  }
}
```

You first define a `Templates` enum, which holds the names of supported template types. You can add more template types to this enum later. You also define a `templates` variable that specifies the React template for each template type. You'll add templates to this variable later.

In the `ResendNotificationProviderService` you add two methods:

- `getTemplate`: Retrieve the template of a template type. If the `html_templates` option is set for the specified template type, you return its `content`'s value. Otherwise, you retrieve the template from the `templates` variable.
- `getTemplateSubject`: Retrieve the subject of a template type. If a `subject` is passed for the template type in the `html_templates`, you return its value. Otherwise, you return a subject based on the template type.

You'll use these methods in the `send` method next.

#### Implement Send Method

In this section, you'll implement the `send` method of `ResendNotificationProviderService`. When you send a notification through the email channel later using the Notification Module, the Notification Module's service will use this `send` method under the hood to send the email with Resend.

In the `send` method, you'll retrieve the template and subject of the email template, then send the email using the Resend client.

Add the `send` method to the `ResendNotificationProviderService` class:

```ts title="src/modules/resend/service.ts" highlights={serviceHighlights3}
// other imports...
import { 
  // ...
  ProviderSendNotificationDTO, 
  ProviderSendNotificationResultsDTO,
} from "@medusajs/framework/types"
import { 
  // ...
  CreateEmailOptions, 
} from "resend"

class ResendNotificationProviderService extends AbstractNotificationProviderService {
  // ...
  async send(
    notification: ProviderSendNotificationDTO
  ): Promise<ProviderSendNotificationResultsDTO> {
    const template = this.getTemplate(notification.template as Templates)

    if (!template) {
      this.logger.error(`Couldn't find an email template for ${notification.template}. The valid options are ${Object.values(Templates)}`)
      return {}
    }

    const commonOptions = {
      from: this.options.from,
      to: [notification.to],
      subject: this.getTemplateSubject(notification.template as Templates),
    }

    let emailOptions: CreateEmailOptions
    if (typeof template === "string") {
      emailOptions = {
        ...commonOptions,
        html: template,
      }
    } else {
      emailOptions = {
        ...commonOptions,
        react: template(notification.data),
      }
    }

    const { data, error } = await this.resendClient.emails.send(emailOptions)

    if (error || !data) {
      if (error) {
        this.logger.error("Failed to send email", error)
      } else {
        this.logger.error("Failed to send email: unknown error")
      }
      return {}
    }

    return { id: data.id }
  }
}
```

The `send` method receives the notification details object as a parameter. Some of its properties include:

- `to`: The address to send the notification to.
- `template`: The template type of the notification.
- `data`: The data useful for the email type. For example, when sending an order-confirmation email, `data` would hold the order's details.

In the method, you retrieve the template and subject of the email using the methods you defined earlier. Then, you put together the data to pass to Resend, such as the email address to send the notification to and the email address to send from.

Also, if the email's template is a string, it's passed as an HTML template. Otherwise, it's passed as a React template.

Finally, you use the `emails.send` method of the Resend client to send the email. If an error occurs you log it in the terminal. Otherwise, you return the ID of the send email as received from Resend. Medusa uses this ID when creating the notification in its database.

### Export Module Definition

The `ResendNotificationProviderService` class now has the methods necessary to start sending emails.

Next, you must export the module provider's definition, which lets Medusa know what module this provider belongs to and its service.

Create the file `src/modules/resend/index.ts` with the following content:

```ts title="src/modules/resend/index.ts"
import { 
  ModuleProvider, 
  Modules,
} from "@medusajs/framework/utils"
import ResendNotificationProviderService from "./service"

export default ModuleProvider(Modules.NOTIFICATION, {
  services: [ResendNotificationProviderService],
})
```

You export the module provider's definition using `ModuleProvider` from the Modules SDK. It accepts as a first parameter the name of the module that this provider belongs to, which is the Notification Module. It also accepts as a second parameter an object having a `service` property indicating the provider's service.

### Add Module to Configurations

Finally, to register modules and module providers in Medusa, you must add them to Medusa's configurations.

Medusa's configurations are set in the `medusa-config.ts` file, which is at the root directory of your Medusa application. The configuration object accepts a `modules` array, whose value is an array of modules to add to the application.

Add the `modules` property to the exported configurations in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          {
            resolve: "./src/modules/resend",
            id: "resend",
            options: {
              channels: ["email"],
              api_key: process.env.RESEND_API_KEY,
              from: process.env.RESEND_FROM_EMAIL,
            },
          },
        ],
      },
    },
  ],
})
```

In the `modules` array, you pass a module object having the following properties:

- `resolve`: The NPM package of the Notification Module. Since the Resend Module is a Notification Module Provider, it'll be passed in the options of the Notification Module.
- `options`: An object of options to pass to the module. It has a `providers` property which is an array of module providers to register. Each module provider object has the following properties:
  - `resolve`: The path to the module provider to register in the application. It can also be the name of an NPM package.
  - `id`: A unique ID, which Medusa will use along with the `identifier` static property that you set earlier in the class to identify this module provider.
  - `options`: An object of options to pass to the module provider. These are the options you expect and use in the module provider's service. You must also specify the `channels` option, which indicates the channels that this provider sends notifications through.

Some of the module's options, such as the Resend API key, are set in environment variables. So, add the following environment variables to `.env`:

```shell
RESEND_FROM_EMAIL=onboarding@resend.dev
RESEND_API_KEY=
```

Where:

- `RESEND_FROM_EMAIL`: The email to send emails from. If you've configured the custom domain as explained in [Step 2](#step-2-prepare-resend-account), change this email to an email from your custom domain. Otherwise, you can use `onboarding@resend.dev` for development purposes.
- `RESEND_API_KEY` is the API key of your Resend account. To retrieve it:
  - Go to API Keys in the sidebar.
  - Click on the Create API Key button.

![Click on the API keys in the sidebar, then click on the Create API Key button at the top right](https://res.cloudinary.com/dza7lstvk/image/upload/v1732535399/Medusa%20Resources/Screenshot_2024-11-25_at_10.22.25_AM_v4d09s.png)

- In the form that opens, enter a name for the API key (for example, Medusa). You can keep its permissions to Full Access or change it to Sending Access. Once you're done, click Add.

![The form to create an API key with fields for the API key's name, permissions, and domain](https://res.cloudinary.com/dza7lstvk/image/upload/v1732535464/Medusa%20Resources/Screenshot_2024-11-25_at_10.23.26_AM_g7gcuc.png)

- A new pop-up will show with your API key hidden. Copy it before closing the pop-up, since you can't access the key again afterwards. Use its value for the `RESEND_API_KEY` environment variable.

![Click the copy icon to copy the API key](https://res.cloudinary.com/dza7lstvk/image/upload/v1732535791/Medusa%20Resources/Screenshot_2024-11-25_at_10.23.43_AM_divins.png)

Your Resend Module Provider is all set up. You'll test it out in a later section.

***

## Step 5: Add Order Confirmation Template

In this step, you'll add a React template for order confirmation emails. You'll create it using the [react-email](https://github.com/resend/react-email) package you installed earlier. You can follow the same steps for other email templates, such as for customer confirmation.

Create the directory `src/modules/resend/emails` that will hold the email templates. Then, to add the template for order confirmation, create the file `src/modules/resend/emails/order-placed.tsx` with the following content:

```tsx title="src/modules/resend/emails/order-placed.tsx" highlights={templateHighlights} collapsibleLines="1-17" expandMoreLabel="Show Imports"
import { 
  Text, 
  Column, 
  Container, 
  Heading, 
  Html, 
  Img, 
  Row, 
  Section, 
  Tailwind, 
  Head, 
  Preview, 
  Body, 
  Link, 
} from "@react-email/components"
import { BigNumberValue, CustomerDTO, OrderDTO } from "@medusajs/framework/types"

type OrderPlacedEmailProps = {
  order: OrderDTO & {
    customer: CustomerDTO
  }
  email_banner?: {
    body: string
    title: string
    url: string
  }
}

function OrderPlacedEmailComponent({ order, email_banner }: OrderPlacedEmailProps) {
  const shouldDisplayBanner = email_banner && "title" in email_banner

  const formatter = new Intl.NumberFormat([], {
    style: "currency",
    currencyDisplay: "narrowSymbol",
    currency: order.currency_code,
  })

  const formatPrice = (price: BigNumberValue) => {
    if (typeof price === "number") {
      return formatter.format(price)
    }

    if (typeof price === "string") {
      return formatter.format(parseFloat(price))
    }

    return price?.toString() || ""
  }

  return (
    <Tailwind>
      <Html className="font-sans bg-gray-100">
        <Head />
        <Preview>Thank you for your order from Medusa</Preview>
        <Body className="bg-white my-10 mx-auto w-full max-w-2xl">
          {/* Header */}
          <Section className="bg-[#27272a] text-white px-6 py-4">
            <svg width="15" height="15" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M16.2447 3.92183L12.1688 1.57686C10.8352 0.807712 9.20112 0.807712 7.86753 1.57686L3.77285 3.92183C2.45804 4.69098 1.63159 6.11673 1.63159 7.63627V12.345C1.63159 13.8833 2.45804 15.2903 3.77285 16.0594L7.84875 18.4231C9.18234 19.1923 10.8165 19.1923 12.15 18.4231L16.2259 16.0594C17.5595 15.2903 18.3672 13.8833 18.3672 12.345V7.63627C18.4048 6.11673 17.5783 4.69098 16.2447 3.92183ZM10.0088 14.1834C7.69849 14.1834 5.82019 12.3075 5.82019 10C5.82019 7.69255 7.69849 5.81657 10.0088 5.81657C12.3191 5.81657 14.2162 7.69255 14.2162 10C14.2162 12.3075 12.3379 14.1834 10.0088 14.1834Z" fill="currentColor"></path></svg>
          </Section>

          {/* Thank You Message */}
          <Container className="p-6">
            <Heading className="text-2xl font-bold text-center text-gray-800">
              Thank you for your order, {order.customer?.first_name || order.shipping_address?.first_name}
            </Heading>
            <Text className="text-center text-gray-600 mt-2">
              We're processing your order and will notify you when it ships.
            </Text>
          </Container>

          {/* Promotional Banner */}
          {shouldDisplayBanner && (
            <Container
              className="mb-4 rounded-lg p-7"
              style={{
                background: "linear-gradient(to right, #3b82f6, #4f46e5)",
              }}
            >
              <Section>
                <Row>
                  <Column align="left">
                    <Heading className="text-white text-xl font-semibold">
                      {email_banner.title}
                    </Heading>
                    <Text className="text-white mt-2">{email_banner.body}</Text>
                  </Column>
                  <Column align="right">
                    <Link href={email_banner.url} className="font-semibold px-2 text-white underline">
                      Shop Now
                    </Link>
                  </Column>
                </Row>
              </Section>
            </Container>
          )}

          {/* Order Items */}
          <Container className="px-6">
            <Heading className="text-xl font-semibold text-gray-800 mb-4">
              Your Items
            </Heading>
            <Row>
              <Column>
                <Text className="text-sm m-0 my-2 text-gray-500">Order ID: #{order.display_id}</Text>
              </Column>
            </Row>
            {order.items?.map((item) => (
              <Section key={item.id} className="border-b border-gray-200 py-4">
                <Row>
                  <Column className="w-1/3">
                    <Img
                      src={item.thumbnail ?? ""}
                      alt={item.product_title ?? ""}
                      className="rounded-lg"
                      width="100%"
                    />
                  </Column>
                  <Column className="w-2/3 pl-4">
                    <Text className="text-lg font-semibold text-gray-800">
                      {item.product_title}
                    </Text>
                    <Text className="text-gray-600">{item.variant_title}</Text>
                    <Text className="text-gray-800 mt-2 font-bold">
                      {formatPrice(item.total)}
                    </Text>
                  </Column>
                </Row>
              </Section>
            ))}

            {/* Order Summary */}
            <Section className="mt-8">
              <Heading className="text-xl font-semibold text-gray-800 mb-4">
                Order Summary
              </Heading>
              <Row className="text-gray-600">
                <Column className="w-1/2">
                  <Text className="m-0">Subtotal</Text>
                </Column>
                <Column className="w-1/2 text-right">
                  <Text className="m-0">
                    {formatPrice(order.item_total)}
                  </Text>
                </Column>
              </Row>
              {order.shipping_methods?.map((method) => (
                <Row className="text-gray-600" key={method.id}>
                  <Column className="w-1/2">
                    <Text className="m-0">{method.name}</Text>
                  </Column>
                  <Column className="w-1/2 text-right">
                    <Text className="m-0">{formatPrice(method.total)}</Text>
                  </Column>
                </Row>
              ))}
              <Row className="text-gray-600">
                <Column className="w-1/2">
                  <Text className="m-0">Tax</Text>
                </Column>
                <Column className="w-1/2 text-right">
                  <Text className="m-0">{formatPrice(order.tax_total || 0)}</Text>
                </Column>
              </Row>
              <Row className="border-t border-gray-200 mt-4 text-gray-800 font-bold">
                <Column className="w-1/2">
                  <Text>Total</Text>
                </Column>
                <Column className="w-1/2 text-right">
                  <Text>{formatPrice(order.total)}</Text>
                </Column>
              </Row>
            </Section>
          </Container>

          {/* Footer */}
          <Section className="bg-gray-50 p-6 mt-10">
            <Text className="text-center text-gray-500 text-sm">
              If you have any questions, reply to this email or contact our support team at support@medusajs.com.
            </Text>
            <Text className="text-center text-gray-500 text-sm">
              Order Token: {order.id}
            </Text>
            <Text className="text-center text-gray-400 text-xs mt-4">
              © {new Date().getFullYear()} Medusajs, Inc. All rights reserved.
            </Text>
          </Section>
        </Body>
      </Html>
    </Tailwind >
  )
}

export const orderPlacedEmail = (props: OrderPlacedEmailProps) => (
  <OrderPlacedEmailComponent {...props} />
)
```

You define the `OrderPlacedEmailComponent` which is a React email template that shows the order's details, such as items and their totals. The component accepts an `order` object as a prop.

You also export an `orderPlacedEmail` function, which accepts props as an input and returns the `OrderPlacedEmailComponent` passing it the props. Because you can't use JSX syntax in `src/modules/resend/service.ts`, you'll import this function instead.

Next, update the `templates` variable in `src/modules/resend/service.ts` to assign this template to the `order-placed` template type:

```ts title="src/modules/resend/service.ts"
// other imports...
import { orderPlacedEmail } from "./emails/order-placed"

const templates: {[key in Templates]?: (props: unknown) => React.ReactNode} = {
  [Templates.ORDER_PLACED]: orderPlacedEmail,
}
```

The `ResendNotificationProviderService` will now use the `OrderPlacedEmailComponent` as the template of order confirmation emails.

### Test Email Out

You'll later test out sending the email when an order is placed. However, you can also test out how the email looks like using [React Email's CLI tool](https://react.email/docs/cli).

First, install the CLI tool in your Medusa application:

```bash npm2yarn
npm install -D react-email
```

Then, in `src/modules/resend/emails/order-placed.tsx`, add the following at the end of the file:

```tsx title="src/modules/resend/emails/order-placed.tsx"
const mockOrder = {
  "order": {
    "id": "order_01JSNXDH9BPJWWKVW03B9E9KW8",
    "display_id": 1,
    "email": "afsaf@gmail.com",
    "currency_code": "eur",
    "total": 20,
    "subtotal": 20,
    "discount_total": 0,
    "shipping_total": 10,
    "tax_total": 0,
    "item_subtotal": 10,
    "item_total": 10,
    "item_tax_total": 0,
    "customer_id": "cus_01JSNXD6VQC1YH56E4TGC81NWX",
    "items": [
      {
        "id": "ordli_01JSNXDH9C47KZ43WQ3TBFXZA9",
        "title": "L",
        "subtitle": "Medusa Sweatshirt",
        "thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png",
        "variant_id": "variant_01JSNXAQCZ5X81A3NRSVFJ3ZHQ",
        "product_id": "prod_01JSNXAQBQ6MFV5VHKN420NXQW",
        "product_title": "Medusa Sweatshirt",
        "product_description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.",
        "product_subtitle": null,
        "product_type": null,
        "product_type_id": null,
        "product_collection": null,
        "product_handle": "sweatshirt",
        "variant_sku": "SWEATSHIRT-L",
        "variant_barcode": null,
        "variant_title": "L",
        "variant_option_values": null,
        "requires_shipping": true,
        "is_giftcard": false,
        "is_discountable": true,
        "is_tax_inclusive": false,
        "is_custom_price": false,
        "metadata": {},
        "raw_compare_at_unit_price": null,
        "raw_unit_price": {
          "value": "10",
          "precision": 20,
        },
        "created_at": new Date(),
        "updated_at": new Date(),
        "deleted_at": null,
        "tax_lines": [],
        "adjustments": [],
        "compare_at_unit_price": null,
        "unit_price": 10,
        "quantity": 1,
        "raw_quantity": {
          "value": "1",
          "precision": 20,
        },
        "detail": {
          "id": "orditem_01JSNXDH9DK1XMESEZPADYFWKY",
          "version": 1,
          "metadata": null,
          "order_id": "order_01JSNXDH9BPJWWKVW03B9E9KW8",
          "raw_unit_price": null,
          "raw_compare_at_unit_price": null,
          "raw_quantity": {
            "value": "1",
            "precision": 20,
          },
          "raw_fulfilled_quantity": {
            "value": "0",
            "precision": 20,
          },
          "raw_delivered_quantity": {
            "value": "0",
            "precision": 20,
          },
          "raw_shipped_quantity": {
            "value": "0",
            "precision": 20,
          },
          "raw_return_requested_quantity": {
            "value": "0",
            "precision": 20,
          },
          "raw_return_received_quantity": {
            "value": "0",
            "precision": 20,
          },
          "raw_return_dismissed_quantity": {
            "value": "0",
            "precision": 20,
          },
          "raw_written_off_quantity": {
            "value": "0",
            "precision": 20,
          },
          "created_at": new Date(),
          "updated_at": new Date(),
          "deleted_at": null,
          "item_id": "ordli_01JSNXDH9C47KZ43WQ3TBFXZA9",
          "unit_price": null,
          "compare_at_unit_price": null,
          "quantity": 1,
          "fulfilled_quantity": 0,
          "delivered_quantity": 0,
          "shipped_quantity": 0,
          "return_requested_quantity": 0,
          "return_received_quantity": 0,
          "return_dismissed_quantity": 0,
          "written_off_quantity": 0,
        },
        "subtotal": 10,
        "total": 10,
        "original_total": 10,
        "discount_total": 0,
        "discount_subtotal": 0,
        "discount_tax_total": 0,
        "tax_total": 0,
        "original_tax_total": 0,
        "refundable_total_per_unit": 10,
        "refundable_total": 10,
        "fulfilled_total": 0,
        "shipped_total": 0,
        "return_requested_total": 0,
        "return_received_total": 0,
        "return_dismissed_total": 0,
        "write_off_total": 0,
        "raw_subtotal": {
          "value": "10",
          "precision": 20,
        },
        "raw_total": {
          "value": "10",
          "precision": 20,
        },
        "raw_original_total": {
          "value": "10",
          "precision": 20,
        },
        "raw_discount_total": {
          "value": "0",
          "precision": 20,
        },
        "raw_discount_subtotal": {
          "value": "0",
          "precision": 20,
        },
        "raw_discount_tax_total": {
          "value": "0",
          "precision": 20,
        },
        "raw_tax_total": {
          "value": "0",
          "precision": 20,
        },
        "raw_original_tax_total": {
          "value": "0",
          "precision": 20,
        },
        "raw_refundable_total_per_unit": {
          "value": "10",
          "precision": 20,
        },
        "raw_refundable_total": {
          "value": "10",
          "precision": 20,
        },
        "raw_fulfilled_total": {
          "value": "0",
          "precision": 20,
        },
        "raw_shipped_total": {
          "value": "0",
          "precision": 20,
        },
        "raw_return_requested_total": {
          "value": "0",
          "precision": 20,
        },
        "raw_return_received_total": {
          "value": "0",
          "precision": 20,
        },
        "raw_return_dismissed_total": {
          "value": "0",
          "precision": 20,
        },
        "raw_write_off_total": {
          "value": "0",
          "precision": 20,
        },
      },
    ],
    "shipping_address": {
      "id": "caaddr_01JSNXD6W0TGPH2JQD18K97B25",
      "customer_id": null,
      "company": "",
      "first_name": "safasf",
      "last_name": "asfaf",
      "address_1": "asfasf",
      "address_2": "",
      "city": "asfasf",
      "country_code": "dk",
      "province": "",
      "postal_code": "asfasf",
      "phone": "",
      "metadata": null,
      "created_at": "2025-04-25T07:25:48.801Z",
      "updated_at": "2025-04-25T07:25:48.801Z",
      "deleted_at": null,
    },
    "billing_address": {
      "id": "caaddr_01JSNXD6W0V7RNZH63CPG26K5W",
      "customer_id": null,
      "company": "",
      "first_name": "safasf",
      "last_name": "asfaf",
      "address_1": "asfasf",
      "address_2": "",
      "city": "asfasf",
      "country_code": "dk",
      "province": "",
      "postal_code": "asfasf",
      "phone": "",
      "metadata": null,
      "created_at": "2025-04-25T07:25:48.801Z",
      "updated_at": "2025-04-25T07:25:48.801Z",
      "deleted_at": null,
    },
    "shipping_methods": [
      {
        "id": "ordsm_01JSNXDH9B9DDRQXJT5J5AE5V1",
        "name": "Standard Shipping",
        "description": null,
        "is_tax_inclusive": false,
        "is_custom_amount": false,
        "shipping_option_id": "so_01JSNXAQA64APG6BNHGCMCTN6V",
        "data": {},
        "metadata": null,
        "raw_amount": {
          "value": "10",
          "precision": 20,
        },
        "created_at": new Date(),
        "updated_at": new Date(),
        "deleted_at": null,
        "tax_lines": [],
        "adjustments": [],
        "amount": 10,
        "order_id": "order_01JSNXDH9BPJWWKVW03B9E9KW8",
        "detail": {
          "id": "ordspmv_01JSNXDH9B5RAF4FH3M1HH3TEA",
          "version": 1,
          "order_id": "order_01JSNXDH9BPJWWKVW03B9E9KW8",
          "return_id": null,
          "exchange_id": null,
          "claim_id": null,
          "created_at": new Date(),
          "updated_at": new Date(),
          "deleted_at": null,
          "shipping_method_id": "ordsm_01JSNXDH9B9DDRQXJT5J5AE5V1",
        },
        "subtotal": 10,
        "total": 10,
        "original_total": 10,
        "discount_total": 0,
        "discount_subtotal": 0,
        "discount_tax_total": 0,
        "tax_total": 0,
        "original_tax_total": 0,
        "raw_subtotal": {
          "value": "10",
          "precision": 20,
        },
        "raw_total": {
          "value": "10",
          "precision": 20,
        },
        "raw_original_total": {
          "value": "10",
          "precision": 20,
        },
        "raw_discount_total": {
          "value": "0",
          "precision": 20,
        },
        "raw_discount_subtotal": {
          "value": "0",
          "precision": 20,
        },
        "raw_discount_tax_total": {
          "value": "0",
          "precision": 20,
        },
        "raw_tax_total": {
          "value": "0",
          "precision": 20,
        },
        "raw_original_tax_total": {
          "value": "0",
          "precision": 20,
        },
      },
    ],
    "customer": {
      "id": "cus_01JSNXD6VQC1YH56E4TGC81NWX",
      "company_name": null,
      "first_name": null,
      "last_name": null,
      "email": "afsaf@gmail.com",
      "phone": null,
      "has_account": false,
      "metadata": null,
      "created_by": null,
      "created_at": "2025-04-25T07:25:48.791Z",
      "updated_at": "2025-04-25T07:25:48.791Z",
      "deleted_at": null,
    },
  },
}
// @ts-ignore
export default () => <OrderPlacedEmailComponent {...mockOrder} />
```

You create a mock order object that contains the order's details. Then, you export a default function that returns the `OrderPlacedEmailComponent` passing it the mock order.

The React Email CLI tool will use the function to render the email template.

Finally, add the following script to `package.json`:

```json
{
  "scripts": {
    "dev:email": "email dev --dir ./src/modules/resend/emails"
  }
}
```

This script will run the React Email CLI tool, passing it the directory where the email templates are located.

You can now test out the email template by running the following command:

```bash npm2yarn
npm run dev:email
```

This will start a development server at `http://localhost:3000`. If you open this URL, you can view your email templates in the browser.

You can make changes to the email template, and the server will automatically reload the changes.

![The email template rendered in the browser](https://res.cloudinary.com/dza7lstvk/image/upload/v1745568201/Medusa%20Resources/Screenshot_2025-04-25_at_10.41.26_AM_u86abc.png)

***

## Step 6: Send Email when Order is Placed

Medusa has an event system that emits an event when a commerce operation is performed. You can then listen and handle that event in an asynchronous function called a subscriber.

So, to send a confirmation email when a customer places an order, which is a commerce operation that Medusa already implements, you don't need to extend or hack your way into Medusa's implementation as you would do with other commerce platforms.

Instead, you'll create a subscriber that listens to the `order.placed` event and sends an email when the event is emitted.

Learn more about Medusa's event system in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).

### Send Order Confirmation Email Workflow

To send the order confirmation email, you need to retrieve the order's details first, then use the Notification Module's service to send the email. To implement this flow, you'll create a workflow.

A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in a subscriber.

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md)

#### Send Notification Step

You'll start by implementing the step of the workflow that sends the notification. To do that, create the file `src/workflows/steps/send-notification.ts` with the following content:

```ts title="src/workflows/steps/send-notification.ts"
import { Modules } from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { CreateNotificationDTO } from "@medusajs/framework/types"

export const sendNotificationStep = createStep(
  "send-notification",
  async (data: CreateNotificationDTO[], { container }) => {
    const notificationModuleService = container.resolve(
      Modules.NOTIFICATION
    )
    const notification = await notificationModuleService.createNotifications(data)
    return new StepResponse(notification)
  }
)
```

You define the `sendNotificationStep` using the `createStep` function that accepts two parameters:

- A string indicating the step's unique name.
- The step's function definition as a second parameter. It accepts the step's input as a first parameter, and an object of options as a second.

The `container` property in the second parameter is an instance of the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools, such as a module's service, that you can resolve to utilize their functionalities.

The Medusa container is accessible by all customizations, such as workflows and subscribers, except for modules. Each module has its own container with Framework tools like the Logger utility.

In the step function, you resolve the Notification Module's service, and use its `createNotifications` method, passing it the notification's data that the step receives as an input.

The step returns an instance of `StepResponse`, which must be returned by any step. It accepts as a parameter the data to return to the workflow that executed this step.

#### Workflow Implementation

You'll now create the workflow that uses the `sendNotificationStep` to send the order confirmation email.

Create the file `src/workflows/send-order-confirmation.ts` with the following content:

```ts title="src/workflows/send-order-confirmation.ts" highlights={workflowHighlights}
import { 
  createWorkflow, 
  when,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { sendNotificationStep } from "./steps/send-notification"

type WorkflowInput = {
  id: string
}

export const sendOrderConfirmationWorkflow = createWorkflow(
  "send-order-confirmation",
  ({ id }: WorkflowInput) => {
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "id",
        "display_id",
        "email",
        "currency_code",
        "total",
        "items.*",
        "shipping_address.*",
        "billing_address.*",
        "shipping_methods.*",
        "customer.*",
        "total",
        "subtotal",
        "discount_total",
        "shipping_total",
        "tax_total",
        "item_subtotal",
        "item_total",
        "item_tax_total",
      ],
      filters: {
        id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })
    
    const notification = when({ orders }, (data) => !!data.orders[0].email)
    .then(() => {
      return sendNotificationStep([{
        to: orders[0].email!,
        channel: "email",
        template: "order-placed",
        data: {
          order: orders[0],
        },
      }])
    })

    return new WorkflowResponse({
      notification,
    })
  }
)
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function, which is the workflow's implementation. The workflow has the following steps:

1. `useQueryGraphStep`, which is a step implemented by Medusa that uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), a tool that allows you to retrieve data across modules. You use it to retrieve the order's details.
2. Ensure that the order has an email address by using `when-then`. If so, you send the notification using the `sendNotificationStep` you implemented earlier. You pass it an object with the following properties:
   - `to`: The address to send the email to. You pass the customer's email that is stored in the order.
   - `channel`: The channel to send the notification through, which is `email`. Since you specified `email` in the Resend Module Provider's `channel` option, the Notification Module will delegate the sending to the Resend Module Provider's service.
   - `template`: The email's template type. You retrieve the template content in the `ResendNotificationProviderService`'s `send` method based on the template specified here.
   - `data`: The data to pass to the email template, which is the order's details.

`when` allows you to perform steps based on a condition during execution. Learn more in the [Conditions in Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) documentation.

You'll execute the workflow when you create the subscriber next.

#### Add the Order Placed Subscriber

Now that you have the workflow to send an order-confirmation email, you'll execute it in a subscriber that's executed whenever an order is placed.

You create a subscriber in a TypeScript or JavaScript file under the `src/subscribers` directory. So, create the file `src/subscribers/order-placed.ts` with the following content:

```ts title="src/subscribers/order-placed.ts" highlights={subscriberHighlights}
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation"

export default async function orderPlacedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await sendOrderConfirmationWorkflow(container)
    .run({
      input: {
        id: data.id,
      },
    })
}

export const config: SubscriberConfig = {
  event: "order.placed",
}
```

A subscriber file exports:

- An asynchronous function that's executed whenever the associated event is emitted, which is the `order.placed` event.
- A configuration object with an `event` property indicating the event the subscriber is listening to.

The subscriber function accepts the event's details as a first parameter which has a `data` property that holds the data payload of the event. For example, Medusa emits the `order.placed` event with the order's ID in the data payload. The function also accepts as a second parameter the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

In the function, you execute the `sendOrderConfirmationWorkflow` by invoking it, passing it the `container`, then using its `run` method. The `run` method accepts an object having an `input` property, which is the input to pass to the workflow. You pass the ID of the placed order as received in the event's data payload.

This subscriber now runs whenever an order is placed. You'll see this in action in the next section.

***

## Test it Out: Place an Order

To test out the Resend integration, you'll place an order using the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) that you installed as part of installing Medusa.

Start your Medusa application first:

```bash npm2yarn
npm run dev
```

Then, in the Next.js Starter Storefront's directory (which was installed in a directory outside of the Medusa application's directory with the name `{project-name}-storefront`, where `{project-name}` is the name of the Medusa application's directory), run the following command to start the storefront:

```bash npm2yarn
npm run dev
```

Then, open the storefront in your browser at `http://localhost:8000` and:

1. Go to Menu -> Store.

![Choose Store from Menu](https://res.cloudinary.com/dza7lstvk/image/upload/v1732539139/Medusa%20Resources/Screenshot_2024-11-25_at_2.51.59_PM_fubiwj.png)

2\. Click on a product, select its options, and add it to the cart.

![Choose an option, such as size, then click on the Add to cart button](https://res.cloudinary.com/dza7lstvk/image/upload/v1732539227/Medusa%20Resources/Screenshot_2024-11-25_at_2.53.11_PM_iswcjy.png)

3\. Click on Cart at the top right, then click Go to Cart.

![Cart is at the top right. It opens a dropdown with a Go to Cart button](https://res.cloudinary.com/dza7lstvk/image/upload/v1732539354/Medusa%20Resources/Screenshot_2024-11-25_at_2.54.44_PM_b1pnlu.png)

4\. On the cart's page, click on the "Go to checkout" button.

![The Go to checkout button is at the right side of the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1732539443/Medusa%20Resources/Screenshot_2024-11-25_at_2.56.27_PM_cvqshj.png)

5\. On the checkout page, when entering the shipping address, make sure to set the email to your Resend account's email if you didn't set up a custom domain.

![Enter your Resend account email if you didn't set up a custom domain](https://res.cloudinary.com/dza7lstvk/image/upload/v1732539536/Medusa%20Resources/Screenshot_2024-11-25_at_2.58.31_PM_wmlh60.png)

6\. After entering the shipping address, choose a delivery and payment methods, then click the Place Order button.

Once the order is placed, you'll find the following message logged in the Medusa application's terminal:

```bash
info:    Processing order.placed which has 1 subscribers
```

This indicates that the `order.placed` event was emitted and its subscriber, which you added in the previous step, is executed.

If you check the inbox of the email address you specified in the shipping address, you'll find a new email with the order's details.

![Example of order-confirmation email](https://res.cloudinary.com/dza7lstvk/image/upload/v1732551372/Medusa%20Resources/Screenshot_2024-11-25_at_6.15.59_PM_efyuoj.png)

***

## Next Steps

You've now integrated Medusa with Resend. You can add more templates for other emails, such as customer registration confirmation, user invites, and more. Check out the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for a list of all events that the Medusa application emits.

### More Resend Email Templates

Find more email templates to use with the Resend Module Provider in the following guides:

- [Send Invite User Email](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/invite-user-subscriber/index.html.md).
- [Send Reset Password Email](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/reset-password/index.html.md).

### Learn More About Medusa

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Integrate Medusa with Sanity (CMS)

In this guide, you'll learn how to integrate Medusa with Sanity.

When you install a Medusa application, you get a fully-fledged commerce platform with support for customizations. While Medusa allows you to manage basic content, such as product description and images, you might need rich content-management features, such as localized content. The Medusa Framework supports you in integrating a CMS with these features.

Sanity is a CMS that simplifies managing content from third-party sources into a single interface. By integrating it with Medusa, you can manage your storefront and commerce-related content, such as product details, from a single interface. You also benefit from advanced content-management features, such as live-preview editing.

This guide will teach you how to:

- Install and set up Medusa.
- Install and set up Sanity with Medusa's Next.js Starter storefront.
- Sync product data from Medusa to Sanity when a product is created or updated.
- Customize the Medusa Admin dashboard to check the sync status and trigger syncing products to Sanity.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer. This guide also assumes you're familiar with Sanity concepts, which you can learn about in [their documentation](https://www.sanity.io/docs).

[Example Repository](https://github.com/medusajs/examples/tree/main/sanity-integration): Find the full code of the guide in this repository.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when you're asked whether you want to install the Next.js Starter Storefront, choose `Y` for yes.

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credential and submit the form.

Afterwards, you can login with the new user and explore the dashboard. The Next.js Starter Storefront is also running at `http://localhost:8000`.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Install Sanity Client SDK

In this step, you'll install [Sanity's JavaScript client SDK](https://www.sanity.io/docs/js-client) in the Medusa application, which you'll use later in your code when sending requests to Sanity.

In your terminal, move to the Medusa application's directory and run the following command:

```bash npm2yarn
cd project-name # replace with directory name
npm install @sanity/client
```

***

## Step 3: Create a Sanity Project

When the Medusa application connects to Sanity, it must connect to a project in Sanity.

So, before building the integration in Medusa, create a project in Sanity using their website:

1. [Sign in or sign up on the Sanity website.](https://www.sanity.io/login)
2. On your account's dashboard, click the "Create new project" button.

![The Create new project button is at the top of the dashboard page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091565/Medusa%20Resources/Screenshot_2024-11-20_at_10.31.10_AM_vvq7y6.png)

3. Enter a project name and click "Create Project"

![A pop-up form will open where you can choose project name and organization.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091565/Medusa%20Resources/Screenshot_2024-11-20_at_10.32.33_AM_xb0rsn.png)

You'll go back to the project's setting page in a later step.

***

## Step 4: Create Sanity Module

To integrate third-party services into Medusa, you create a custom module. A module is a re-usable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

In this step, you'll create a Sanity Module that provides the interface to connect to and interact with Sanity. In later steps, you'll use the functionalities provided by this module to sync products to Sanity or retrieve documents from it.

Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).

### Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/sanity`.

### Create Service

You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, which is useful if your module defines tables in the database, or connect to a third-party service.

Medusa registers the module's service in the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), allowing you to easily resolve the service from other customizations and use its methods.

The Medusa application registers resources, such as a module's service or the [logging tool](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md), in the Medusa container so that you can resolve them from other customizations, as you'll see in later sections. Learn more about it in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

In this section, you'll create the Sanity Module's service and the methods necessary to connect to Sanity.

Start by creating the file `src/modules/sanity/service.ts` with the following content:

```ts title="src/modules/sanity/service.ts"
import {
  Logger,
} from "@medusajs/framework/types"
import {
  SanityClient,
} from "@sanity/client"

class SanityModuleService {
  private client: SanityClient
  private studioUrl?: string
  private logger: Logger

  // TODO
}

export default SanityModuleService
```

You create the `SanityModuleService` class that for now only has three properties:

- `client` property of type `SanityClient` (from the Sanity SDK you installed in the previous step) to send requests to Sanity.
- `studioUrl` property which will hold the URL to access the Sanity studio.
- `logger` property, which is an instance of Medusa's [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md), to log messages.

In the service, you want to initialize the client early-on so that you can use it in the service's methods. This requires options to be passed to the client, like the Sanity API key or project ID.

So, add after the import at the top of the file the following types:

```ts title="src/modules/sanity/service.ts"
// other imports...

const SyncDocumentTypes = {
  PRODUCT: "product",
} as const

type SyncDocumentTypes =
  (typeof SyncDocumentTypes)[keyof typeof SyncDocumentTypes];

type ModuleOptions = {
  api_token: string;
  project_id: string;
  api_version: string;
  dataset: "production" | "development";
  type_map?: Record<SyncDocumentTypes, string>;
  studio_url?: string;
}
```

The `ModuleOptions` type defines the type of options that the module expects:

- `api_token`: API token to connect to Sanity.
- `project_id`: The ID of the Sanity project.
- `api_version`: The Sanity API version.
- `dataset`: The dataset to use, which is either `production` or `development`.
- `type_map`: The types to sync from Medusa to Sanity. For simplicity, this guide only covers syncing products, but you can support other data types like product categories, too.
- `studio_url`: The URL to the Sanity studio. This is used to show the studio URL later in the Medusa Admin dashboard.

You can now initialize the client, which you'll do in the `constructor` of the `SanityModuleService`:

```ts title="src/modules/sanity/service.ts"
import {
  // other imports...
  createClient,
} from "@sanity/client"

// types...

type InjectedDependencies = {
  logger: Logger
};

class SanityModuleService {
  // properties...
  constructor({
    logger,
  }: InjectedDependencies, options: ModuleOptions) {
    this.client = createClient({
      projectId: options.project_id,
      apiVersion: options.api_version,
      dataset: options.dataset,
      token: options.api_token,
    })
    this.logger = logger

    this.logger.info("Connected to Sanity")

    this.studioUrl = options.studio_url
    
    // TODO initialize more properties
  }
}
```

The service's constructor accepts two parameters:

1. Resources to resolve from the Module's container. A module has a different container than the Medusa application, which you can learn more about it in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md).
2. The options passed to the module.

In the constructor, you create a Sanity client using the `createClient` function imported from `@sanity/client`. You pass it the options that the module receives.

You also initialize the `logger` and `studioUrl` properties, and log a message indicating that connection to Sanity was successful.

#### Transform Product Data

When you create or update products in Sanity, you must prepare the product object based on what Sanity expects.

So, you'll add methods to the service that transform a Medusa product to a Sanity document object.

Start by adding the following types and class properties to `src/modules/sanity/service.ts`:

```ts title="src/modules/sanity/service.ts"
type SyncDocumentInputs<T> = T extends "product"
  ? ProductDTO
  : never

type TransformationMap<T> = Record<
  SyncDocumentTypes,
  (data: SyncDocumentInputs<T>) => any
>;

class SanityModuleService {
  // other properties...
  private typeMap: Record<SyncDocumentTypes, string>
  private createTransformationMap: TransformationMap<SyncDocumentTypes>
  private updateTransformationMap: TransformationMap<SyncDocumentTypes>

  // ...
}
```

First, you define types for a transformation map, which is a map that pairs up a document type (such as `product`) to a function that handles transforming its data.

Then, in the service, you define three new properties:

- `typeMap`: Pair of `SyncDocumentTypes` values (for example, `product`) and their type name in Sanity.
- `createTransformationMap`: Pair of `SyncDocumentTypes` values (for example, `product`) and the method used to transform a Medusa product to a Sanity document data to be created.
- `updateTransformationMap`: Pair of `SyncDocumentTypes` values (for example, `product`) and the method used to transform a Medusa product to a Sanity update operation.

Next, add the following two methods to transform a product:

```ts title="src/modules/sanity/service.ts"
// other imports...
import {
  ProductDTO,
} from "@medusajs/framework/types"

class SanityModuleService {
  // ...
  private transformProductForCreate = (product: ProductDTO) => {
    return {
      _type: this.typeMap[SyncDocumentTypes.PRODUCT],
      _id: product.id,
      title: product.title,
      specs: [
        {
          _key: product.id,
          _type: "spec",
          title: product.title,
          lang: "en",
        },
      ],
    }
  }

  private transformProductForUpdate = (product: ProductDTO) => {
    return {
      set: {
        title: product.title,
      },
    }
  }
}
```

The `transformProductForCreate` method accepts a product and returns an object that you'll later pass to Sanity to create the product document. Similarly, the `transformProductForUpdate` method accepts a product and returns an object that you'll later pass to Sanity to update the product document.

The Sanity document's schema type will be defined in a later chapter. If you add other fields to it, make sure to edit these methods.

Finally, initialize the new properties you added in the `SanityModuleService`'s constructor:

```ts title="src/modules/sanity/service.ts"
class SanityModuleService {
  // ...
  constructor({
    logger,
  }: InjectedDependencies, options: ModuleOptions) {
    // ...
    this.typeMap = Object.assign(
      {},
      {
        [SyncDocumentTypes.PRODUCT]: "product",
      },
      options.type_map || {}
    )

    this.createTransformationMap = {
      [SyncDocumentTypes.PRODUCT]: this.transformProductForCreate,
    }

    this.updateTransformationMap = {
      [SyncDocumentTypes.PRODUCT]: this.transformProductForUpdate,
    }
  }
  // ...
}
```

You initialize the `typeMap` property to map the `product` type in Medusa to the `product` schema type in Sanity. You also initialize the `createTransformationMap` and `updateTransformationMap` to map the methods to transform a product for creation or update.

You can modify these properties to add support for other schema types, such as product categories or collections.

#### Methods to Manage Documents

In this section, you'll add the methods that accept data from Medusa and create or update them as documents in Sanity.

Add the following methods to the `SanityModuleService` class:

```ts title="src/modules/sanity/service.ts" highlights={syncMethodsHighlights}
// other imports...
import {
  // ...
  FirstDocumentMutationOptions,
} from "@sanity/client"

class SanityModuleService {
  // ...
  async upsertSyncDocument<T extends SyncDocumentTypes>(
    type: T,
    data: SyncDocumentInputs<T>
  ) {
    const existing = await this.client.getDocument(data.id)
    if (existing) {
      return await this.updateSyncDocument(type, data)
    }

    return await this.createSyncDocument(type, data)
  }

  async createSyncDocument<T extends SyncDocumentTypes>(
    type: T,
    data: SyncDocumentInputs<T>,
    options?: FirstDocumentMutationOptions
  ) {
    const doc = this.createTransformationMap[type](data)
    return await this.client.create(doc, options)
  }

  async updateSyncDocument<T extends SyncDocumentTypes>(
    type: T,
    data: SyncDocumentInputs<T>
  ) {
    const operations = this.updateTransformationMap[type](data)
    return await this.client.patch(data.id, operations).commit()
  }
}
```

You add three methods:

- `upsertSyncDocument`: Creates or updates a document in Sanity for a data type in Medusa.
- `createSyncDocument`: Creates a document in Sanity for a data type in Medusa. It uses the `createTransformationMap` property to use the transform method of the specified Medusa data type (for example, a product's data).
- `updateSyncDocument`: Updates a document in Sanity for a data type in Medusa. It uses the `updateTransformationMap` property to use the transform method of the specified Medusa data type (for example, a product's data).

You also need methods to manage the Sanity documents without transformations. So, add the following methods to `SanityModuleService`:

```ts title="src/modules/sanity/service.ts" highlights={methodsHighlights}
class SanityModuleService {
  // ...
  async retrieve(id: string) {
    return this.client.getDocument(id)
  }

  async delete(id: string) {
    return this.client.delete(id)
  }

  async update(id: string, data: any) {
    return await this.client.patch(id, {
      set: data,
    }).commit()
  }

  async list(
    filter: {
      id: string | string[]
    }
  ) {
    const data = await this.client.getDocuments(
      Array.isArray(filter.id) ? filter.id : [filter.id]
    )

    return data.map((doc) => ({
      id: doc?._id,
      ...doc,
    }))
  }
}
```

You add other three methods:

- `retrieve` to retrieve a document by its ID.
- `delete` to delete a document by its ID.
- `update` to update a document by its ID with new data.
- `list` to list documents, with ability to filter them by their IDs. Since a Sanity document's ID is a product's ID, you can pass product IDs as a filter to retrieve their documents.

### Export Module Definition

The `SanityModuleService` class now has the methods necessary to connect to and perform actions in Sanity.

Next, you must export the Module definition, which lets Medusa know what the Module's name is and what is its service.

Create the file `src/modules/sanity/index.ts` with the following content:

```ts title="src/modules/sanity/index.ts"
import { Module } from "@medusajs/framework/utils"
import SanityModuleService from "./service"

export const SANITY_MODULE = "sanity"

export default Module(SANITY_MODULE, {
  service: SanityModuleService,
})
```

In the file, you export the `SANITY_MODULE` which is the Module's name. You'll use it later when you resolve the module from the Medusa container.

You also export the module definition using `Module` from the Modueles SDK, which accepts as a first parameter the module's name, and as a second parameter an object having a `service` property, indicating the module's service.

### Add Module to Configurations

Finally, to register a module in Medusa, you must add it to Medusa's configurations.

Medusa's configurations are set in the `medusa-config.ts` file, which is at the root directory of your Medusa application. The configuration object accepts a `modules` array, whose value is an array of modules to add to the application.

Add the `modules` property to the exported configurations in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/sanity",
      options: {
        api_token: process.env.SANITY_API_TOKEN,
        project_id: process.env.SANITY_PROJECT_ID,
        api_version: new Date().toISOString().split("T")[0],
        dataset: "production",
        studio_url: process.env.SANITY_STUDIO_URL || 
          "http://localhost:3000/studio",
        type_map: {
          product: "product",
        },
      },
    },
  ],
})
```

In the `modules` array, you pass a module object having the following properties:

- `resolve`: The path to the module to register in the application. It can also be the name of an NPM package.
- `options`: An object of options to pass to the module. These are the options you expect and use in the module's service.

Some of the module's options, such as the Sanity API key, are set in environment variables. So, add the following environment variables to `.env`:

```shell
SANITY_API_TOKEN=
SANITY_PROJECT_ID=
SANITY_STUDIO_URL=http://localhost:8000/studio
```

Where:

- `SANITY_API_TOKEN`: The API key token to connect to Sanity, which you can retrieve from the Sanity project's dashboard:
  - Go to the API tab.

![The API tab is at the top of the project dashboard next to Settings.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091810/Medusa%20Resources/Screenshot_2024-11-20_at_10.35.29_AM_ltj7cd.png)

- Scroll down to Tokens and click on the "Add API Token" button.

![The Add API token button is at the top right of the Tokens section.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091810/Medusa%20Resources/Screenshot_2024-11-20_at_10.35.52_AM_ccgsum.png)

- Enter a name for the API token, choose "Editor" for the permissions, then click Save.

![In the Token form, enter the name and choose "Editor" for permissions.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091811/Medusa%20Resources/Screenshot_2024-11-20_at_10.36.25_AM_nqxa5y.png)

- `SANITY_PROJECT_ID`: The ID of the project, which you can find at the top section of your Sanity project's dashboard.

![The project ID is in the top information of the project.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091988/Medusa%20Resources/Screenshot_2024-11-20_at_10.39.24_AM_cscir8.png)

- `SANITY_STUDIO_URL`: The URL to access the studio. You'll set the studio up in a later section, but for now set it to `http://localhost:8000/studio`.

### Test the Module

To test that the module is working, you'll start the Medusa application and see if the "Connected to Sanity" message is logged in the console.

To start the Medusa application, run the following command in the application's directory:

```bash npm2yarn
npm run dev
```

If you see the following message among the logs:

```bash
info:    Connected to Sanity
```

That means your Sanity credentials were correct, and Medusa was able to connect to Sanity.

In the next steps, you'll create a link between the Product and Sanity modules to retrieve data between them easily, and build a flow around the Sanity Module to sync data.

***

## Step 5: Link Product and Sanity Modules

Since a product has a document in Sanity, you want to build an association between the [Product](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md) and Sanity modules so that when you retrieve a product, you also retrieve its associated Sanity document.

However, modules are [isolated](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md) to ensure they're re-usable and don't have side effects when integrated into the Medusa application. So, to build associations between modules, you define [module links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md).

A Module Link associates two modules' data models while maintaining module isolation. A data model can be a table in the database or a virtual model from an external systems.

In this section, you'll define a link between the Product and Sanity modules.

Links are defined in a TypeScript or JavaScript file under the `src/links` directory. So, create the file `src/links/product-sanity.ts` with the following content:

```ts title="src/links/product-sanity.ts"
import { defineLink } from "@medusajs/framework/utils"
import ProductModule from "@medusajs/medusa/product"
import { SANITY_MODULE } from "../modules/sanity"

defineLink(
  {
    linkable: ProductModule.linkable.product.id,
    field: "id",
  },
  {
    linkable: {
      serviceName: SANITY_MODULE,
      alias: "sanity_product",
      primaryKey: "id",
    },
  },
  {
    readOnly: true,
  }
)
```

You define a link using `defineLink` from the Modules SDK. It accepts three parameters:

1. The first data model part of the link. In this case, it's the Product Module's `product` data model. A module has a special `linkable` property that contain link configurations for its data models.
2. The second data model part of the link. Since the Sanity Module doesn't have a Medusa data model, you specify the configurations in a `linkable` object that has the following properties:
   - `serviceName`: The registration name in the Medusa container of the service managing the data model, which in this case is the Sanity Module's name (since the module's service is registered under that name).
   - `alias`: The name to refer to the model part of this link, such as when retrieving the Sanity document of a product. You'll use this in a later section.
   - `primaryKey`: The name of the data model's primary key field.
3. An object of configurations for the module link. By default, Medusa creates a table in the database to represent the link you define. Since the module link isn't created between two Medusa data models, you enable the `readOnly` configuration, which will tell Medusa not to create a table in the database for this link.

In the next steps, you'll see how this link allows you to retrieve documents when retrieving products.

***

## Step 6: Sync Data to Sanity

After integrating Sanity with a custom module, you now want to sync product data from Medusa to Sanity, automatically and manually. To implement the sync logic, you need a workflow.

A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. You'll see how all of this works in the upcoming sections.

Within a workflow's steps, you resolve modules to use their service's functionalities as part of a bigger flow. Then, you can execute the workflow from other customizations, such as in response to an event or in an API route.

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md)

In this section, you'll create a workflow that syncs products from Medusa to Sanity. Later, you'll execute this workflow when a product is created or updated, or when an admin user triggers the syncing manually.

### Create Step

The syncing workflow will have a single step that syncs products provided as an input to Sanity.

So, to implement that step, create the file `src/workflows/sanity-sync-products/steps/sync.ts` with the following content:

If you get a type error on resolving the Sanity Module, run the Medusa application once with the `npm run dev` or `yarn dev` command to generate the necessary type definitions, as explained in the [Automatically Generated Types guide](https://docs.medusajs.com/docs/learn/fundamentals/generated-types/index.html.md).

```ts title="src/workflows/sanity-sync-products/steps/sync.ts" highlights={syncStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { ProductDTO } from "@medusajs/framework/types"
import { 
  ContainerRegistrationKeys,
  promiseAll,
} from "@medusajs/framework/utils"
import SanityModuleService from "../../../modules/sanity/service"
import { SANITY_MODULE } from "../../../modules/sanity"

export type SyncStepInput = {
  product_ids?: string[];
}

export const syncStep = createStep(
  { name: "sync-step", async: true },
  async (input: SyncStepInput, { container }) => {
    const sanityModule: SanityModuleService = container.resolve(SANITY_MODULE)
    const query = container.resolve(ContainerRegistrationKeys.QUERY)

    const total = 0
    const upsertMap: {
      before: any
      after: any
    }[] = []

    const batchSize = 200
    const hasMore = true
    const offset = 0
    const filters = input.product_ids ? {
      id: input.product_ids,
    } : {}

    while (hasMore) {
      const {
        data: products,
        metadata: { count } = {},
      } = await query.graph({
        entity: "product",
        fields: [
          "id",
          "title",
          "sanity_product.*",
        ],
        filters,
        pagination: {
          skip: offset,
          take: batchSize,
          order: {
            id: "ASC",
          },
        },
      })

      // TODO sync products
    }
  }
)
```

You define the `syncStep` using the `createStep` function, which accepts two parameters:

- An object of step configurations. The object must have the `name` property, which is this step's unique name. Enabling the `async` property means that the workflow should run asynchronously in the background. This is useful when the workflow is triggered manually through an HTTP request, meaning the response will be returned to the client even if the workflow hasn't finished executing.
- The step's function definition as a second parameter.

The step function accepts the step's input as a first parameter, and an object of options as a second. The object of options has a `container` property, which is an instance of the Medusa container that you can use to resolve resources.

In the step, you resolve from the Medusa container Sanity Module's service and [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which is a tool that allows you to retrieve data across modules and links.

You use Query's `graph` method to retrieve products, filtering them by their IDs and applying pagination configurations. The `graph` method accepts a `fields` property in its object parameter, which indicates the product data model's fields and relations to retrieve.

Notice that you pass `sanity_product.*` in the `fields` array. Medusa will retrieve the Sanity document of each product using Sanity Module's `list` method and attach it to the returned product. So, you don't have to retrieve the products and documents separately. Each product object in the returned array will look similar to this:

```json title="Example Product Object"
{
  "id": "prod_123",
  "title": "Shirt",
  "sanity_product": {
    "id": "prod_123",
    "_type": "product",
    // other Sanity fields...
  }
}
```

Next, you want to sync the retrieved products. So, replace the `TODO` in the `while` loop with the following:

```ts title="src/workflows/sanity-sync-products/steps/sync.ts"
while (hasMore) {
  // ...
  try {
    await promiseAll(
      products.map(async (prod) => {
        const after = await sanityModule.upsertSyncDocument(
          "product", 
          prod as unknown as ProductDTO
        )

        upsertMap.push({
          // @ts-ignore
          before: prod.sanity_product,
          after,
        })

        return after
      })
    )
  } catch (e) {
    return StepResponse.permanentFailure(
      `An error occurred while syncing documents: ${e}`,
      upsertMap
    )
  }

  offset += batchSize
  hasMore = offset < count
  total += products.length
}
```

In the `while` loop, you loop over the array of products to sync them to Sanity. You use the `promiseAll` Medusa utility that loops over an array of promises and ensures that all transactions within these promises are rolled back in case an error occurs.

For each product, you upsert it into Sanity, then push its document before and after the update to the `upsertMap`. You'll learn more about its use later.

You also wrap the `promiseAll` function within a try-catch block. In the catch block, you invoke and return `StepResponse.permanentFailure` which indicates that the step has failed but still invokes the rollback mechanism that you'll implement in a bit. The first parameter of `permanentFailure` is the error message, and the second is the data to use in the rollback mechanism.

Finally, after the `while` loop and at the end of the step, add the following return statement:

```ts title="src/workflows/sanity-sync-products/steps/sync.ts"
return new StepResponse({ total }, upsertMap)
```

If no errors occur, the step returns an instance of `StepResponse`, which must be returned by any step. It accepts as a first parameter the data to return to the workflow that executed this step.

#### Add Compensation Function

`StepResponse` accepts a second parameter, which is passed to the compensation function. A compensation function defines the rollback logic of a step, and it's only executed if an error occurs in the workflow. This eliminates data inconsistency if an error occurs and the workflow can't finish execution successfully.

Learn more about compensation functions in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/compensation-function/index.html.md).

The `syncStep` creates or updates products in Sanity. So, the compensation function must delete created documents or revert the update of a document to its previous data. The compensation function is only executed if an error occurs.

To define the compensation function, pass a third-parameter to the `createStep` function:

```ts title="src/workflows/sanity-sync-products/steps/sync.ts"
export const syncStep = createStep(
  { name: "sync-step", async: true },
  async (input: SyncStepInput, { container }) => {
    // ...
  },
  async (upsertMap, { container }) => {
    if (!upsertMap) {
      return
    }

    const sanityModule: SanityModuleService = container.resolve(SANITY_MODULE)

    await promiseAll(
      upsertMap.map(({ before, after }) => {
        if (!before) {
          // delete the document
          return sanityModule.delete(after._id)
        }

        const { _id: id, ...oldData } = before

        return sanityModule.update(
          id,
          oldData
        )
      })
    )
  }
)
```

The compensation function accepts the data passed in the step's `StepResponse` second parameter (in this case, `upsertMap`), and an object of options similar to that of the step.

In the compensation function, you resolve the Sanity Module's service, then loop over the `upsertMap` to delete created documents, or revert existing ones.

### Create Workflow

You'll now create the workflow that uses the `syncStep`. This is the workflow that you'll later execute to sync data automatically or manually.

Workflows are created in a file under the `src/workflows` directory. So, create the file `src/workflows/sanity-sync-products/index.ts` with the following content:

```ts title="src/workflows/sanity-sync-products/index.ts"
import {
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { syncStep } from "./steps/sync"

export type SanitySyncProductsWorkflowInput = {
  product_ids?: string[];
};

export const sanitySyncProductsWorkflow = createWorkflow(
  { name: "sanity-sync-products", retentionTime: 10000 },
  function (input: SanitySyncProductsWorkflowInput) {
    const result = syncStep(input)

    return new WorkflowResponse(result)
  }
)
```

You create a workflow using the `createWorkflow` from the Workflows SDK. It accepts an object of options as a first parameter, where the `name` property is required and indicates the workflow's unique name.

The `retentionTime` property indicates how long should the workflow's progress be saved in the database. This is useful if you later want to track whether the workflow is successfully executing.

`createWorkflow` accepts as a second parameter a constructor function, which is the workflow's implementation. In the function, you execute the `syncStep` to sync the specified products in the input, then return its result. Workflows must return an instance of `WorkflowResponse`.

A workflow's constructor function has some constraints in implementation. Learn more about them in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md).

You'll execute and test this workflow in the next steps.

***

## Step 7: Handle Product Changes in Medusa

You've defined the workflow to sync the products. Now, you want to execute it when a product is created or updated.

Medusa emits events when certain actions occur, such as when a product is created. Then, you can listen to those events in a subscriber.

A subscriber is an asynchronous function that listens to one or more events. Then, when those events are emitted, the subscriber is executed in the background of your application.

Subscribers are useful when you want to perform an action that isn't an integral part of a flow, but as a reaction to a performed action. In this case, syncing the products to Sanity isn't integral to creating a product, so you do it in a subscriber after the product is created.

Learn more about events and subscribers in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). You can also find the list of emitted events in [this reference](https://docs.medusajs.com/references/events/index.html.md).

So, to run the workflow you defined in the previous event when a product is created or updated, you'll create a subscriber that listens to the `product.created` and `product.updated` events.

Subscribers are created under the `src/subscribers` directory. So, create the file `src/subscribers/sanity-product-sync.ts` with the following content:

```ts title="src/subscribers/sanity-product-sync.ts" highlights={subscriberHighlights}
import type { 
  SubscriberArgs, 
  SubscriberConfig,
} from "@medusajs/medusa"
import { 
  sanitySyncProductsWorkflow,
} from "../workflows/sanity-sync-products"

export default async function upsertSanityProduct({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await sanitySyncProductsWorkflow(container).run({
    input: {
      product_ids: [data.id],
    },
  })
}

export const config: SubscriberConfig = {
  event: ["product.created", "product.updated"],
}
```

The subscriber function `upsertSanityProduct` accepts an object as a parameter that has the following properties:

- `event`: An object of the event's details. Its `data` property holds the data payload emitted with the event, which in this case is the ID of the product created or updated.
- `container`: An instance of the Medusa container to resolve resources.

In the subscriber, you execute the `sanitySyncProductsWorkflow` by invoking it, passing it the container, then invoking its `run` method. You pass the workflow's input in the `input` property of the `run`'s object parameter.

The subscriber file must also export a configuration object. It has an `event` property, which is the names of the events that the subscriber is listening to.

### Test it Out

To test it out, run the Medusa application, then open the Medusa Admin in your browser at `http://localhost:9000/app`. Try creating or updating a product. You'll see the following message in the console:

```bash
info:    Processing product.created which has 1 subscribers
```

This means that the `product.created` event was emitted and your subscriber was executed.

In the next step, you'll setup Sanity with Next.js, and you can then monitor the updates in Sanity's studio.

***

## Step 8: Setup Sanity with Next.js Starter Storefront

In this step, you'll install Sanity in the Next.js Starter and configure it. You'll then have a Sanity studio in your Next.js Starter Storefront, where you'll later view the product documents being synced from Medusa, and update their content that you'll display in the storefront on the product details page.

Sanity has a CLI tool that helps you with the setup. First, change to the Next.js Starter's directory (it's outside the Medusa application's directory and its name is `{project-name}-storefront`, where `{project-name}` is the name of the Medusa application's directory).

Then, run the following command:

```bash badgeLabel="Storefront" badgeColor="blue"
npx sanity@latest init
```

You'll then be asked a few questions:

- For the project, select the Sanity project you created earlier in this guide.
- For dataset, use `production` unless you changed it in the Sanity project.
- Select yes for adding the Sanity configuration files to the Next.js folder.
- Select yes for TypeScript.
- Select yes for Sanity studio, and choose the `/studio` route.
- Select clean project template.
- Select yes for adding the project ID and dataset to `.env.local`.

Afterwards, the command will install the necessary dependencies for Sanity.

### Error during installation

If you run into an error during the installation of peer dependencies, try running the following command to install them:

```bash
yarn add next-sanity@9.8.15 @sanity/client@^6.22.4 @sanity/icons@^3.4.0 @sanity/types@^3.62.0 @sanity/ui@^2.8.10 next@^15.0.0 react@^19.0.0 react-dom@^19.0.0 sanity@^3.62.0 styled-components@^6.1
```

### Update Middleware

The Next.js Starter storefront has a middleware that ensures all requests start with a country code (for example, `/us`).

Since the Sanity studio runs at `/studio`, the middleware should ignore requests to this path.

Open the file `src/middleware.ts` and find the following `if` condition:

```ts title="src/middleware.ts" badgeLabel="Storefront" badgeColor="blue"
if (
  urlHasCountryCode &&
  (!isOnboarding || onboardingCookie) &&
  (!cartId || cartIdCookie)
) {
  return NextResponse.next()
}
```

Replace it with the following condition:

```ts title="src/middleware.ts" badgeLabel="Storefront" badgeColor="blue"
if (
  request.nextUrl.pathname.startsWith("/studio") ||
  urlHasCountryCode &&
  (!isOnboarding || onboardingCookie) &&
  (!cartId || cartIdCookie)
) {
  return NextResponse.next()
}
```

If the path starts with `/studio`, the middleware will stop executing and the page will open.

### Set CORS Settings

Every Sanity project has a configured set of CORS origins allowed, with the default being `http://localhost:3333`.

The Next.js Starter runs on the `8000` port, so you must add it to the allowed CORS origins.

In your Sanity project's dashboard:

1. Click on the API tab.

![Find the API tab at the top of the dashboard.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732096643/Medusa%20Resources/Screenshot_2024-11-20_at_10.35.29_AM_ltj7cd.png)

2. Scroll down to CORS origins and click the "Add CORS origin" button.

![Find the CORS origins section and click the Add CORS origin button at its top right.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732096997/Medusa%20Resources/Screenshot_2024-11-20_at_12.02.50_PM_ahsthb.png)

3. Enter `http://localhost:8000` in the Origin field.
4. Enable the "Allow credentials" checkbox.

![After filling out the Origin field, click on the Allow credentials checkbox to enable it.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732097074/Medusa%20Resources/Screenshot_2024-11-20_at_12.04.09_PM_nxdvwh.png)

5. Click the Save button.

### Open Sanity Studio

To open the Sanity studio, start the Next.js Starter's development server:

```bash npm2yarn
npm run dev
```

Then, open `http://localhost:8000/studio` in your browser. The Sanity studio will open, but right now it's empty.

***

## Step 9: Add Product Schema Type in Sanity

In this step, you'll define the `product` schema type in Sanity. You' can then view the documents of that schema in the studio and update their content.

To create the schema type, create the file `src/sanity/schemaTypes/documents/product.ts` with the following content:

```ts title="src/sanity/schemaTypes/documents/product.ts" badgeLabel="Storefront" badgeColor="blue"
import { ComposeIcon } from "@sanity/icons"
import { DocumentDefinition } from "sanity"

const productSchema: DocumentDefinition = {
  fields: [
    {
      name: "title",
      type: "string",
    },
    {
      group: "content",
      name: "specs",
      of: [
        {
          fields: [
            { name: "lang", title: "Language", type: "string" },
            { name: "title", title: "Title", type: "string" },
            {
              name: "content",
              rows: 3,
              title: "Content",
              type: "text",
            },
          ],
          name: "spec",
          type: "object",
        },
      ],
      type: "array",
    },
    {
      fields: [
        { name: "title", title: "Title", type: "string" },
        {
          name: "products",
          of: [{ to: [{ type: "product" }], type: "reference" }],
          title: "Addons",
          type: "array",
          validation: (Rule) => Rule.max(3),
        },
      ],
      name: "addons",
      type: "object",
    },
  ],
  name: "product",
  preview: {
    select: {
      title: "title",
    },
  },
  title: "Product Page",
  type: "document",
  groups: [{
    default: true,
    // @ts-ignore
    icon: ComposeIcon,
    name: "content",
    title: "Content",
  }],
}

export default productSchema
```

This creates a schema that has the following fields:

- `title`: The title of a document, which is in this case the product's type.
- `specs`: An array of product specs. Each object in the array has the following fields:
  - `lang`: This is useful if you want to have localized content.
  - `title`: The product's title.
  - `content`: Textual content, such as the product's description.
- `addons`: An object of products related to this product.

When you sync the products from Medusa, you only sync the title. You manage the `specs` and `addons` fields within Sanity.

Next, replace the content of `src/sanity/schemaTypes/index.ts` with the following:

```ts title="src/sanity/schemaTypes/index.ts" badgeLabel="Storefront" badgeColor="blue"
import { SchemaPluginOptions } from "sanity"
import productSchema from "./documents/product"

export const schema: SchemaPluginOptions = {
  types: [productSchema],
  templates: (templates) => templates.filter(
    (template) => template.schemaType !== "product"
  ),
}
```

You add the product schema to the list of exported schemas, but also disable creating a new product. You can only create the products in Medusa.

### Test it Out

To ensure that your schema is defined correctly and working, start the Next.js Starter Storefront's server, and open the Sanity studio again at `http://localhost:8000/studio`.

You'll find "Product Page" under Content. If you click on it, you'll find any product you've synced from Medusa.

If you haven't synced any products yet or you want to see the live update, try now creating or updating a product in Medusa. You'll find it added in the Sanity studio.

If you click on any product, you can edit its existing field under "Specs" or add new ones. In the next section, you'll learn how to show the content in the "Specs" field on the storefront's product details page.

***

## Step 10: Show Sanity Content in Next.js Starter Storefront

Now that you're managing a product's content in Sanity, you want to show that content on the storefront. In this step, you'll customize the Next.js Starter storefront to show a product's content as defined in Sanity.

A product's details are retrieved in the file `src/app/[countryCode]/(main)/products/[handle]/page.tsx`. So, replace the `ProductPage` function with the following:

```tsx title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={sanityContentHighlights}
// other imports...
import { client } from "../../../../../sanity/lib/client"

// ...

export default async function ProductPage(props: Props) {
  const params = await props.params
  const region = await getRegion(params.countryCode)

  if (!region) {
    notFound()
  }

  const pricedProduct = await listProducts({
    countryCode: params.countryCode,
    queryParams: { handle: params.handle },
  }).then(({ response }) => response.products[0])

  if (!pricedProduct) {
    notFound()
  }

  // alternatively, you can filter the content by the language
  const sanity = (await client.getDocument(pricedProduct.id))?.specs[0]

  return (
    <ProductTemplate
      product={pricedProduct}
      region={region}
      countryCode={params.countryCode}
    />
  )
}
```

You import the Sanity client defined in `src/sanity/lib/client.ts` (this was generated by Sanity's CLI). Then, in the page's function, you retrieve the product's document by ID and pass its first step to the `ProductTemplate` component.

This is a simplified approach, but you can also have languages in your storefront and filter the spec based on the current language.

Next, you need to customize the `ProductTemplate` to accept the `sanity` prop. In the file `src/modules/products/templates/index.tsx` add the following to `ProductTemplateProps`:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
type ProductTemplateProps = {
  // ...
  sanity?: {
    content: string
  }
}
```

Then, add the `sanity` property to the expanded props of the component:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const ProductTemplate: React.FC<ProductTemplateProps> = ({
  // ...
  sanity,
}) => {
  // ...
}
```

Finally, pass the `sanity` prop to the `ProductInfo` component in the return statement:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
<ProductInfo product={product} sanity={sanity} />
```

Next, you need to update the `ProductInfo` component to accept and use the `sanity` prop.

In `src/modules/products/templates/product-info/index.tsx`, update the `ProductInfoProps` to accept the `sanity` prop:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
type ProductInfoProps = {
  // ...
  sanity?: {
    content: string
  }
}
```

Then, add the `sanity` property to the expanded props of the component:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const ProductInfo = ({ 
  // ...
  sanity,
}: ProductInfoProps) => {
  // ...
}
```

Next, find the following line in the return statement:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
{product.description}
```

And replace it with the following:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
{sanity?.content || product.description}
```

Instead of showing the product's description on the product details page, this will show the content defined in Sanity if available.

### Test it Out

To test this out, first, run both the Next.js Starter storefront and the Medusa application, and open the Sanity studio. Try editing the content of the first spec of a product.

Then, open the Next.js Starter storefront at `http://localhost:8000` and go to "Store" from the menu, then select the product you edited in Sanity.

In the product's page, you'll find under the product's name the content you put in Sanity.

You can now manage the product's content in Sanity, add more fields, and customize how you show them in the storefront. The Medusa application will also automatically create documents in Sanity for new products you add or update, ensuring your products are always synced across systems.

***

## Step 11: Customize Admin to Manually Sync Data

There are cases where you need to trigger the syncing of products manually, such as when an error occurs or you have products from before creating this integration.

The Medusa Admin dashboard is customizable, allowing you to either inject components, called [widgets](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md), into existing pages, or adding new pages, called [UI routes](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md). In these customizations, you can send requests to the Medusa application to perform custom operations.

In this step, you'll add a widget to the product's details page. In that page, you'll show whether a product is synced with Sanity, and allow the admin user to trigger syncing it manually.

![The widget in the product details page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732093722/Medusa%20Resources/Screenshot_2024-11-20_at_11.08.23_AM_wzftfv.png)

Before you do that, however, you need two new API routes in your Medusa application: one to retrieve a document from Sanity, and one to trigger syncing the product data.

An API route is a REST API endpoint that exposes commerce features to the admin dashboard or other frontend clients. Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

### Get Sanity Document API Route

In this section, you'll create the API route to retrieve a sanity document, and the URL to it in the Sanity studio.

To retrieve the URL to the Sanity studio, add the following method in the Sanity Module's service in `src/modules/sanity/service.ts`:

```ts title="src/modules/sanity/service.ts"
class SanityModuleService {
  // ...
  async getStudioLink(
    type: string,
    id: string,
    config: { explicit_type?: boolean } = {}
  ) {
    const resolvedType = config.explicit_type ? type : this.typeMap[type]
    if (!this.studioUrl) {
      throw new Error("No studio URL provided")
    }
    return `${this.studioUrl}/structure/${resolvedType};${id}`
  }
}
```

The method uses the `studioUrl` property, which you set in the `constructor` using the `studio_url` module option, to get the studio link.

Then, to create the API route, create the file `src/api/admin/sanity/documents/[id]/route.ts` with the following content:

```ts title="src/api/admin/sanity/documents/[id]/route.ts"
import { 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import SanityModuleService from "src/modules/sanity/service"
import { SANITY_MODULE } from "../../../../../modules/sanity"

export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
  const { id } = req.params

  const sanityModule: SanityModuleService = req.scope.resolve(
    SANITY_MODULE
  )
  const sanityDocument = await sanityModule.retrieve(id)

  const url = sanityDocument ? 
    await sanityModule.getStudioLink(
      sanityDocument._type,
      sanityDocument._id,
      { explicit_type: true }
    )
    : ""

  res.json({ sanity_document: sanityDocument, studio_url: url })
}
```

This defines a `GET` API route at `/admin/sanity/documents/:id`, where `:id` is a dynamic path parameter indicating the ID of a document to retrieve.

In the `GET` route handler, you resolve the Sanity Module's service and use it to first retrieve the product's document, then the studio link of that document.

You return in the JSON response an object having the `sanity_document` and `studio_url` properties.

You'll test out this route in a later section.

Since the API route is added under the `/admin` prefix, only authenticated admin users can access it. Learn more about protected routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md).

### Trigger Sanity Sync API Route

In this section, you'll add the API route that manually triggers syncing a product to Sanity.

Since you already have the workflow to sync products, you only need to create an API route that executes it.

Create the file `src/api/admin/sanity/documents/[id]/sync/route.ts` with the following content:

```ts title="src/api/admin/sanity/documents/[id]/sync/route.ts"
import { 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { 
  sanitySyncProductsWorkflow,
} from "../../../../../../workflows/sanity-sync-products"

export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
  const { transaction } = await sanitySyncProductsWorkflow(req.scope)
    .run({
      input: { product_ids: [req.params.id] },
    })

  res.json({ transaction_id: transaction.transactionId })
}
```

You add a `POST` API route at `/admin/sanity/documents/:id/sync`, where `:id` is a dynamic path parameter that indicates the ID of a product to sync to Sanity.

In the `POST` API route handler, you execute the `sanitySyncProductsWorkflow`, passing it the ID of the product from the path parameter as an input.

In the next section, you'll customize the admin dashboard and send requests to the API route from there.

### Sanity Product Widget

In this section, you'll add a widget in the product details page. The widget will show the Sanity document of the product and triggers syncing it to Sanity using the API routes you created.

To send requests from admin customizations to the Medusa server, you need to use Medusa's [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md). You'll also use [Tanstack Query](https://tanstack.com/query/latest) to benefit from features like data caching and invalidation.

Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency.

To configure the JS SDK, create the file `src/admin/lib/sdk.ts` with the following content:

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

You initialize the JS SDK and export it. You can learn more about configuring the JS SDK in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md).

Next, you'll create hooks using Tanstack Query to send requests to the API routes you created earlier.

Create the file `src/admin/hooks/sanity.tsx` with the following content:

```ts title="src/admin/hooks/sanity.tsx"
import { 
  useMutation, 
  UseMutationOptions, 
  useQueryClient, 
} from "@tanstack/react-query"
import { sdk } from "../lib/sdk"

export const useTriggerSanityProductSync = (
  id: string,
  options?: UseMutationOptions
) => {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: () =>
      sdk.client.fetch(`/admin/sanity/documents/${id}/sync`, {
        method: "post",
      }),
    onSuccess: (data: any, variables: any, context: any) => {
      queryClient.invalidateQueries({
        queryKey: [`sanity_document`, `sanity_document_${id}`],
      })

      options?.onSuccess?.(data, variables, context)
    },
    ...options,
  })
}
```

You define the `useTriggerSanityProductSync` hook which creates a Tanstack Query mutation that, when executed, sends a request to the API route that triggers syncing the product to Sanity.

Add in the same file another hook:

```ts title="src/admin/hooks/sanity.tsx"
// other imports...
import { 
  // ...
  QueryKey, 
  useQuery, 
  UseQueryOptions,
} from "@tanstack/react-query"
import { FetchError } from "@medusajs/js-sdk"

// ...

export const useSanityDocument = (
  id: string,
  query?: Record<any, any>,
  options?: Omit<
    UseQueryOptions<
      Record<any, any>,
      FetchError,
      { sanity_document: Record<any, any>; studio_url: string },
      QueryKey
    >,
    "queryKey" | "queryFn"
  >
) => {
  const fetchSanityProductStatus = async (query?: Record<any, any>) => {
    return await sdk.client.fetch<Record<any, any>>(
      `/admin/sanity/documents/${id}`,
      {
        query,
      }
    )
  }

  const { data, ...rest } = useQuery({
    queryFn: async () => fetchSanityProductStatus(query),
    queryKey: [`sanity_document_${id}`],
    ...options,
  })

  return { ...data, ...rest }
}
```

You define the hook `useSanityDocument` which retrieves the Sanity document of a product using Tankstack Query.

You can now create the widget injected in a product's details page. Widgets are react components created in a file under the `src/admin/widgets` directory.

So, create the file `src/admin/widgets/sanity-product.tsx` with the following content:

```tsx title="src/admin/widgets/sanity-product.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { AdminProduct, DetailWidgetProps } from "@medusajs/types"
import { ArrowUpRightOnBox } from "@medusajs/icons"
import { Button, CodeBlock, Container, StatusBadge, toast } from "@medusajs/ui"
import { useState } from "react"
import {
  useSanityDocument,
  useTriggerSanityProductSync,
} from "../hooks/sanity"

const ProductWidget = ({ data }: DetailWidgetProps<AdminProduct>) => {
  const { mutateAsync, isPending } = useTriggerSanityProductSync(data.id)
  const { sanity_document, studio_url, isLoading } = useSanityDocument(data.id)
  const [showCodeBlock, setShowCodeBlock] = useState(false)

  const handleSync = async () => {
    try {
      await mutateAsync(undefined)
      toast.success(`Sync triggered.`)
    } catch (err) {
      toast.error(`Couldn't trigger sync: ${
        (err as Record<string, unknown>).message
      }`)
    }
  }

  return (
    <Container>
      <div className="flex justify-between w-full items-center">
        <div className="flex gap-2 items-center">
          <h2>Sanity Status</h2>
          <div>
            {isLoading ? (
              "Loading..."
            ) : sanity_document?.title === data.title ? (
              <StatusBadge color="green">Synced</StatusBadge>
            ) : (
              <StatusBadge color="red">Not Synced</StatusBadge>
            )}
          </div>
        </div>
        <Button
          size="small"
          variant="secondary"
          onClick={handleSync}
          disabled={isPending}
        >
          Sync
        </Button>
      </div>
      <div className="mt-6">
        <div className="mb-4 flex gap-4">
          <Button
            size="small"
            variant="secondary"
            onClick={() => setShowCodeBlock(!showCodeBlock)}
          >
            {showCodeBlock ? "Hide" : "Show"} Sanity Document
          </Button>
          {studio_url && (
            <a href={studio_url} target="_blank" rel="noreferrer">
              <Button variant="transparent">
                <ArrowUpRightOnBox /> Sanity Studio
              </Button>
            </a>
          )}
        </div>
        {!isLoading && showCodeBlock && (
          <CodeBlock
            className="dark"
            snippets={[
              {
                language: "json",
                label: "Sanity Document",
                code: JSON.stringify(sanity_document, null, 2),
              },
            ]}
          >
            <CodeBlock.Body />
          </CodeBlock>
        )}
      </div>
    </Container>
  )
}

// The widget's configurations
export const config = defineWidgetConfig({
  zone: "product.details.after",
})

export default ProductWidget
```

The file exports a `ProductWidget` component and a `config` object created with `defineWidgetConfig` from the Admin Extension SDK. In the `config` object, you specify the zone to inject the widget into in the `zone` property.

Find all widget injection zones in [this reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-widget-injection-zones/index.html.md).

In the widget, you use the `useSanityDocument` to retrieve the product's document from Sanity by sending a request to the API route you created earlier. You show that document's details and a button to trigger syncing the data.

When the "Sync" button is clicked, you use the `useTriggerSanityProductSync` hook which sends a request to the API route you created earlier and executes the workflow that syncs the product to Sanity. The workflow will execute in the background, since you configured its step to be async.

To render a widget that matches the rest of the admin dashboard's design, you use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md), such as the `CodeBlock` or `Container` components.

Refer to the [Admin Widgets](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md) documentation to learn more.

### Test it Out

To test these customizations out, start the Medusa application and open the admin dashboard. Then, choose a product and scroll down to the end of the page.

You'll find a new "Sanity Status" section showing you whether the product is synced to Sanity and its document's details. You can also click the Sync button, which will sync the product to Sanity.

***

## Step 12: Add Track Syncs Page to Medusa Admin

Earlier in this guide when introducing workflows, you learned that you can track the execution of a workflow. As a last step of this guide, you'll add a new page in the admin dashboard that shows the executions of the `sanitySyncProductsWorkflow` and their status. You'll also add the ability to sync all products to Sanity from that page.

![A screenshot of the page to track and trigger syncs.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732095185/Medusa%20Resources/Screenshot_2024-11-20_at_11.09.42_AM_te8xic.png)

### Retrieve Sync Executions API Route

Medusa has a [workflow engine](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/index.html.md) that manages workflow executions, roll-backs, and other functionalities under the hood.

The workflow engine is an [Infrastructure Module](https://docs.medusajs.com/docs/learn/fundamentals/modules/infrastructure-modules/index.html.md), which can be replaced with a [Redis Workflow Engine](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/redis/index.html.md), or a custom one of your choice, allowing you to take ownership of your application's tooling.

In your customizations, you can resolve the workflow engine from the container and manage executions of a workflow, such as retrieve them and check their progress.

In this section, you'll create an API route to retrieve the stored executions of the `sanitySyncProductsWorkflow` workflow, so that you can display them later on the dashboard.

When you defined the `sanitySyncProductsWorkflow`, you set its `retentionTime` option so that you can store the workflow execution's details temporarily. If a workflow doesn't have this option set, its execution won't be stored for tracking.

Create the file `src/api/admin/sanity/syncs/route.ts` with the following content:

```ts title="src/api/admin/sanity/syncs/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"
import { 
  sanitySyncProductsWorkflow,
} from "../../../../workflows/sanity-sync-products"

export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
  const workflowEngine = req.scope.resolve(
    Modules.WORKFLOW_ENGINE
  )

  const [executions, count] = await workflowEngine
    .listAndCountWorkflowExecutions(
      {
        workflow_id: sanitySyncProductsWorkflow.getName(),
      },
      { order: { created_at: "DESC" } }
    )

  res.json({ workflow_executions: executions, count })
}
```

You add a `GET` API route at `/admin/sanity/syncs`. In the API route handler, you resolve the Workflow Engine Module's service from the Medusa container. You use the `listAndCountWorkflowExecutions` method to retrieve the executions of the `sanitySyncProductsWorkflow` workflow, filtering by its name.

You return the executions in the JSON response of the route.

### Trigger Sync API Route

In this section, you'll add another API route that triggers syncing all products to Sanity.

In the same file `src/api/admin/sanity/syncs/route.ts`, add the following:

```ts title="api/admin/sanity/syncs/route.ts"
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
  const { transaction } = await sanitySyncProductsWorkflow(req.scope).run({
    input: {},
  })

  res.json({ transaction_id: transaction.transactionId })
}
```

This adds a `POST` API route at `/admin/sanity/syncs`. In the route handler, you execute the `sanitySyncProductsWorkflow` without passing it a `product_ids` input. The step in the workflow will retrieve all products, instead of filtering them by ID, and sync them to Sanity.

You return the transaction ID of the workflow, which you can use to track the execution's progress since the workflow will run in the background. This is not implemented in this guide, but Medusa has a [Get Execution API route](https://docs.medusajs.com/api/admin#workflows-executions_getworkflowsexecutionsworkflow_idtransaction_id) that you can use to get the details of a workflow's execution.

### Add Sanity UI Route

In this section, you'll add a UI route in the admin dashboard, which is a new page, that shows the list of `sanitySyncProductsWorkflow` executions and allows triggering sync of all products in Medusa.

A UI route is React component exported in a file under the `src/admin/routes` directory. Similar to a widget, a UI route can also send requests to the Medusa application to perform actions using your custom API routes.

Before creating the UI route, you'll create hooks using Tanstack Query that send requests to these UI routes. In the file `src/admin/hooks/sanity.tsx`, add the following two new hooks:

```tsx title="src/admin/hooks/sanity.tsx"
export const useTriggerSanitySync = (options?: UseMutationOptions) => {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: () =>
      sdk.client.fetch(`/admin/sanity/syncs`, {
        method: "post",
      }),
    onSuccess: (data: any, variables: any, context: any) => {
      queryClient.invalidateQueries({
        queryKey: [`sanity_sync`],
      })

      options?.onSuccess?.(data, variables, context)
    },
    ...options,
  })
}

export const useSanitySyncs = (
  query?: Record<any, any>,
  options?: Omit<
    UseQueryOptions<
      Record<any, any>,
      FetchError,
      { workflow_executions: Record<any, any>[] },
      QueryKey
    >,
    "queryKey" | "queryFn"
  >
) => {
  const fetchSanitySyncs = async (query?: Record<any, any>) => {
    return await sdk.client.fetch<Record<any, any>>(`/admin/sanity/syncs`, {
      query,
    })
  }

  const { data, ...rest } = useQuery({
    queryFn: async () => fetchSanitySyncs(query),
    queryKey: [`sanity_sync`],
    ...options,
  })

  return { ...data, ...rest }
}
```

The `useTriggerSanitySync` hook creates a mutation that, when executed, sends a request to the trigger sync API route you created earlier to sync all products.

The `useSanitySyncs` hook sends a request to the retrieve sync executions API route that you created earlier to retrieve the workflow's executions.

Finally, to create the UI route, create the file `src/admin/routes/sanity/page.tsx` with the following content:

```tsx title="src/admin/routes/sanity/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Sanity } from "@medusajs/icons"
import {
  Badge,
  Button,
  Container,
  Heading,
  Table,
  Toaster,
  toast,
} from "@medusajs/ui"
import { useSanitySyncs, useTriggerSanitySync } from "../../hooks/sanity"

const SanityRoute = () => {
  const { mutateAsync, isPending } = useTriggerSanitySync()
  const { workflow_executions, refetch } = useSanitySyncs()

  const handleSync = async () => {
    try {
      await mutateAsync()
      toast.success(`Sync triggered.`)
      refetch()
    } catch (err) {
      toast.error(`Couldn't trigger sync: ${
        (err as Record<string, unknown>).message
      }`)
    }
  }

  const getBadgeColor = (state: string) => {
    switch (state) {
      case "invoking":
        return "blue"
      case "done":
        return "green"
      case "failed":
        return "red"
      default:
        return "grey"
    }
  }

  return (
    <>
      <Container className="flex flex-col p-0 overflow-hidden">
        <div className="p-6 flex justify-between">
          <Heading className="font-sans font-medium h1-core">
            Sanity Syncs
          </Heading>
          <Button
            variant="secondary"
            size="small"
            onClick={handleSync}
            disabled={isPending}
          >
            Trigger Sync
          </Button>
        </div>
        <Table>
          <Table.Header>
            <Table.Row>
              <Table.HeaderCell>Sync ID</Table.HeaderCell>
              <Table.HeaderCell>Status</Table.HeaderCell>
              <Table.HeaderCell>Created At</Table.HeaderCell>
              <Table.HeaderCell>Updated At</Table.HeaderCell>
            </Table.Row>
          </Table.Header>

          <Table.Body>
            {(workflow_executions || []).map((execution) => (
              <Table.Row
                key={execution.id}
                className="cursor-pointer"
                onClick={() =>
                  (window.location.href = `/app/sanity/${execution.id}`)
                }
              >
                <Table.Cell>{execution.id}</Table.Cell>
                <Table.Cell>
                  <Badge
                    rounded="full"
                    size="2xsmall"
                    color={getBadgeColor(execution.state)}
                  >
                    {execution.state}
                  </Badge>
                </Table.Cell>
                <Table.Cell>{execution.created_at}</Table.Cell>
                <Table.Cell>{execution.updated_at}</Table.Cell>
              </Table.Row>
            ))}
          </Table.Body>
        </Table>
      </Container>
      <Toaster />
    </>
  )
}

export const config = defineRouteConfig({
  label: "Sanity",
  icon: Sanity,
})

export default SanityRoute
```

The file's path relative to the `src/admin/routes` directory indicates its path in the admin dashboard. So, this adds a new route at the path `http://localhost:9000/app/sanity`.

The file must export the UI route's component. Also, to add an item in the sidebar for the UI route, you export a configuration object, created with `defineRouteConfig` from the Admin Extension SDK. The function accepts the following properties:

- `label`: The sidebar item's label.
- `icon`: The icon to the show in the sidebar.

In the UI route, you use the `useSanitySyncs` hook to retrieve the list of sync executions and display them with their status. You also show a "Trigger Sync" button that, when clicked, uses the mutation from the `useTriggerSanitySync` hook to send a request to the Medusa application and trigger the sync.

To display components that match the design of the Medusa Admin, you use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md).

Learn more about UI routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md).

### Test it Out

To test it out, start the Medusa application and open the admin dashboard. After logging in, you'll find a new "Sanity" item in the sidebar.

If you click on it, you'll see a table of the latest syncs. You also trigger syncing by clicking the "Trigger Sync" button. After you click the button, you should see a new execution added to the table.

***

## Next Steps

You've now integrated Medusa with Sanity and can benefit from powerful commerce and CMS features.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Integrate Segment (Analytics) with Medusa

In this tutorial, you'll learn how to integrate Segment with Medusa to track events and analytics.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. Medusa's architecture facilitates integrating third-party services to customize Medusa's infrastructure for your business needs.

To track analytics in your Medusa application, you can integrate [Segment](https://segment.com/), a service that collects analytics from multiple sources and sends them to various destinations. This tutorial will help you set up Segment in your Medusa application and track common events.

## Summary

By following this tutorial, you'll learn how to:

- Install and set up Medusa.
- Integrate Segment with your Medusa application.
- Handle Medusa's `order.placed` event to track order placements.
- Track custom events in your Medusa application with Segment.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Diagram showcasing the integration of Segment with Medusa](https://res.cloudinary.com/dza7lstvk/image/upload/v1748264333/Medusa%20Book/segment-overview_apkrtp.jpg)

[Example Repository](https://github.com/medusajs/examples/tree/main/segment-integration): Find the full code of the guide in this repository.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

First, you'll be asked for the project's name. Then, when prompted about installing the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Segment Module Provider

To integrate third-party services into Medusa, you create a custom module. A module is a reusable package with functionalities related to a single feature or domain.

Medusa's [Analytics Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/analytics/index.html.md) provides an interface to track events in your Medusa application. It delegates the actual tracking to the configured Analytics Module Provider.

In this step, you'll integrate Segment as an Analytics Module Provider. Later, you'll use it to track events in your Medusa application.

Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules in Medusa.

### a. Install Segment Node SDK

Before you create the Segment Module Provider, you'll install the Segment Node SDK to interact with Segment's API.

Run the following command in your Medusa application's directory:

```bash npm2yarn
npm install @segment/analytics-node
```

You'll use the SDK in the next steps.

### b. Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/segment`.

### c. Create Segment Module's Service

A module has a service that contains its logic. For Analytics Module Providers, the service implements the logic to track events in the third-party service.

To create the service of the Segment Analytics Module Provider, create the file `src/modules/segment/service.ts` with the following content:

```ts title="src/modules/segment/service.ts" highlights={serviceHighlights}
import { 
  AbstractAnalyticsProviderService,
  MedusaError,
} from "@medusajs/framework/utils"
import { Analytics } from "@segment/analytics-node"

type Options = {
  writeKey: string
}

type InjectedDependencies = {}

class SegmentAnalyticsProviderService extends AbstractAnalyticsProviderService {
  private client: Analytics
  static identifier = "segment"

  constructor(container: InjectedDependencies, options: Options) {
    super()
    if (!options.writeKey) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Segment write key is required"
      )
    }
    this.client = new Analytics({ writeKey: options.writeKey })
  }
}

export default SegmentAnalyticsProviderService
```

An Analytics Module Provider's service must extend the `AbstractAnalyticsProviderService` class. It must also have an `identifier` static property with the unique identifier of the provider.

A module provider's constructor receives two parameters:

- `container`: The [module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) that contains Framework resources available to the module. In this tutorial, you don't need to resolve any resources.
- `options`: Options that are passed to the module provider when it's registered in Medusa's configurations. You define the following option:
  - `writeKey`: The Segment write key. You'll learn how to retrieve and set this option in the [Add Module Provider to Medusa's Configurations](#h-add-module-provider-to-medusas-configurations) section.

In the constructor, you create a Segment client using the Segment Node SDK. You pass the `writeKey` option to the client.

You'll use this client to implement the service's methods in the next sections.

Refer to the [Create Analytics Module Provider](https://docs.medusajs.com/references/analytics/provider/index.html.md) guide for detailed information about the methods.

### d. Implement identify Method

The `identify` method is used to identify a user in Segment. It associates the user's ID with their profile information, such as name and email.

Add the `identify` method to the `SegmentAnalyticsProviderService` class:

```ts title="src/modules/segment/service.ts"
// other imports...
import { ProviderIdentifyAnalyticsEventDTO } from "@medusajs/types"

class SegmentAnalyticsProviderService extends AbstractAnalyticsProviderService {
  // ...
  async identify(data: ProviderIdentifyAnalyticsEventDTO): Promise<void> {
    const anonymousId = data.properties && "anonymousId" in data.properties ? 
      data.properties.anonymousId : undefined
    const traits = data.properties && "traits" in data.properties ? 
        data.properties.traits : undefined

    if ("group" in data) {
      this.client.group({
        groupId: data.group.id,
        userId: data.actor_id,
        anonymousId,
        traits,
        context: data.properties,
      })
    } else {
      this.client.identify({
        userId: data.actor_id,
        anonymousId,
        traits,
        context: data.properties,
      })
    }
  }
}
```

#### Parameters

The `identify` method receives an object with the following properties:

- `actor_id`: The ID of the user being identified.
- `group`: Alternatively, the group being identified. If this property is present, the `actor_id` is ignored.
- `properties`: Additional properties to associate with the user or group. This can include traits like name, email, and so on.

The method receives other parameters, which you can find in the [Create Analytics Module Provider](https://docs.medusajs.com/references/analytics/provider#identify/index.html.md) guide.

#### Method Logic

In the method, if the `group` property is present, you call the `group` method of the Segment client to identify a group. Otherwise, you call the `identify` method to identify a user.

For both methods, you extract the `anonymousId` and `traits` from the `properties` object if they are present. You also pass the `actor_id` as the `userId`, and `group.id` for groups.

### e. Implement track Method

The `track` method is used to track events in Segment. It can track events like order placements, cart updates, and more.

Add the `track` method to the `SegmentAnalyticsProviderService` class:

```ts title="src/modules/segment/service.ts"
// other imports...
import { ProviderTrackAnalyticsEventDTO } from "@medusajs/types"

class SegmentAnalyticsProviderService extends AbstractAnalyticsProviderService {
  // ...
  async track(data: ProviderTrackAnalyticsEventDTO): Promise<void> {
    const userId = "group" in data ? 
      data.actor_id || data.group?.id : data.actor_id
    const anonymousId = data.properties && "anonymousId" in data.properties ? 
      data.properties.anonymousId : undefined

    if (!userId && !anonymousId) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA, 
        `Actor or group ID is required for event ${data.event}`
      )
    }

    this.client.track({
      userId,
      anonymousId,
      event: data.event,
      properties: data.properties,
      timestamp: data.properties && "timestamp" in data.properties ? 
        new Date(data.properties.timestamp) : undefined,
    })
  }
}
```

#### Parameters

The `track` method receives an object with the following properties:

- `actor_id`: The ID of the user performing the event.
- `group`: Alternatively, the group performing the event. If this property is present, the `actor_id` is ignored.
- `event`: The name of the event being tracked.
- `properties`: Additional properties associated with the event. This can include details like product ID, order ID, and so on.

The method receives other parameters, which you can find in the [Create Analytics Module Provider](https://docs.medusajs.com/references/analytics/provider#track/index.html.md) guide.

#### Method Logic

In the method, you set the user ID either to the actor or group ID. You also check if the anonymous ID is present in the properties to use it.

Next, you call the `track` method of the Segment client, passing it the user ID, anonymous ID, event name, properties, and timestamp (if present in the properties).

### f. Implement shutdown Method

The `shutdown` method is used to gracefully shut down the Segment client when the Medusa application is stopped. It allows you to send all pending events to Segment before the application exits.

Add the following method to the `SegmentAnalyticsProviderService` class:

```ts title="src/modules/segment/service.ts"
class SegmentAnalyticsProviderService extends AbstractAnalyticsProviderService {
  // ...
  async shutdown(): Promise<void> {
    await this.client.flush({
      close: true,
    })
  }
}
```

#### Method Logic

In the method, you call the `flush` method of the Segment client with the `close` option set to `true`. This method will send all pending events to Segment and close the client connection.

### g. Export Module Definition

You've now finished implementing the necessary methods for the Segment Analytics Module Provider.

The final piece to a module is its definition, which you export in an `index.ts` file at the module's root directory. This definition tells Medusa the module's details, including its service.

To create the module's definition, create the file `src/modules/segment/index.ts` with the following content:

```ts title="src/modules/segment/index.ts"
import SegmentAnalyticsProviderService from "./service"
import { 
  ModuleProvider, 
  Modules,
} from "@medusajs/framework/utils"

export default ModuleProvider(Modules.ANALYTICS, {
  services: [SegmentAnalyticsProviderService],
})
```

You use `ModuleProvider` from the Modules SDK to create the module provider's definition. It accepts two parameters:

1. The name of the module that this provider belongs to, which is `Modules.ANALYTICS` in this case.
2. An object with a required property `services` indicating the Module Provider's services.

### h. Add Module Provider to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/analytics",
      options: {
        providers: [
          {
            resolve: "./src/modules/segment",
            id: "segment",
            options: {
              writeKey: process.env.SEGMENT_WRITE_KEY || "",
            },
          },
        ],
      },
    },
  ],
})
```

To pass an Analytics Module Provider to the Analytics Module, you add the `modules` property to the Medusa configuration and pass the Analytics Module in its value.

The Analytics Module accepts a `providers` option, which is an array of Analytics Module Providers to register. However, you can only register one analytics provider in your Medusa application.

To register the Segment Analytics Module Provider, you add an object to the `providers` array with the following properties:

- `resolve`: The NPM package or path to the module provider. In this case, it's the path to the `src/modules/segment` directory.
- `id`: The ID of the module provider. The Analytics Module Provider is then registered with the ID `aly_{identifier}_{id}`, where:
  - `{identifier}`: The identifier static property defined in the Module Provider's service, which is `segment` in this case.
  - `{id}`: The ID set in this configuration, which is also `segment` in this case.
- `options`: The options to pass to the module provider. These are the options you defined in the `Options` interface of the module provider's service.

### i. Set Option as Environment Variable

Next, you'll set the Segment write key as an environment variable.

To retrieve the Segment write key:

1. Log into your [Segment](https://app.segment.com) account.
2. Go to the Connections page and click the "Add More" button next to the "Sources" section.

![Add more button in the Connections page](https://res.cloudinary.com/dza7lstvk/image/upload/v1748254988/Medusa%20Book/CleanShot_2025-05-26_at_10.47.24_2x_qqlkwk.png)

3. In the "Choose a Source" step, select "Node.js" and click the "Next" button.

![Select Node.js as the Source](https://res.cloudinary.com/dza7lstvk/image/upload/v1748255028/Medusa%20Book/CleanShot_2025-05-26_at_10.47.57_2x_zy9g8k.png)

4. In the "Connect your Node.js Source" step, enter a name for the source and click the "Create Source" button. This will show you the write key to copy.

![Copy the write key](https://res.cloudinary.com/dza7lstvk/image/upload/v1748255065/Medusa%20Book/CleanShot_2025-05-26_at_10.48.41_2x_lpgsbb.png)

You can skip the next step of testing out the source for now.

Then, add the following environment variable to your `.env` file:

```shell
SEGMENT_WRITE_KEY=123...
```

Replace `123...` with the write key you copied from Segment.

You'll test out the integration as you set up event tracking in the next steps.

***

## Step 3: Track Order Placement Event

You'll first track the order-placement event, which is triggered natively in the Medusa application.

Medusa's events system allows you to listen to events triggered by the Medusa application and execute custom logic asynchronously in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).

In the subscriber, you execute functionalities created in [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of actions, called steps, that complete a task.

In this step, you'll create a workflow that tracks the `order.placed` event in Segment. Then, you'll create a subscriber that listens to this event and executes the workflow.

### a. Create Track Event Step

Before you create the workflow, you'll create a step that tracks an event in Segment. Later, you'll use this step in the workflows that track events, such as the order-placement event.

To create a step, create the file `src/workflows/steps/track-event.ts` with the following content:

```ts title="src/workflows/steps/track-event.ts" highlights={stepHighlights}
import { createStep } from "@medusajs/framework/workflows-sdk"

type TrackEventStepInput = {
  event: string
  userId?: string
  properties?: Record<string, unknown>
  timestamp?: Date
}

export const trackEventStep = createStep(
  "track-event",
  async (input: TrackEventStepInput, { container }) => {
    const analyticsModuleService = container.resolve(
      "analytics"
    )

    if (!input.userId) {
      // generate a random user id
      input.properties = {
        ...input.properties,
        anonymousId: Math.random().toString(36).substring(2, 15) + 
          Math.random().toString(36).substring(2, 15),
      }
    }

    await analyticsModuleService.track({
      event: input.event,
      actor_id: input.userId,
      properties: input.properties,
    })
  }
)
```

You create a step with `createStep` from the Workflows SDK. It accepts two parameters:

1. The step's unique name, which is `track-event`.
2. An async function that receives two parameters:
   - The step's input, which is in this case an object with the following properties:
     - `event`: The name of the event to track.
     - `userId`: The ID of the user performing the event.
     - `properties`: Additional properties associated with the event.
     - `timestamp`: The timestamp of the event (optional).
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.

The Medusa container is different from the module's container. Since modules are isolated, they each have a container with their resources. Refer to the [Module Container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) documentation for more information.

In the step function, you resolve the Analytics Module's service from the Medusa container. This service is the interface to track events with the configured Analytics Module Provider, which is Segment in this case.

If the `userId` is not provided, you generate a random anonymous ID and add it to the properties. This is useful for tracking events from users who are not logged in.

Finally, you call the `track` method of the Analytics Module's service, passing it the event name, user ID, and properties.

### b. Create Track Order Placed Workflow

Next, you'll create the workflow that tracks the order placement event.

To create the workflow, create the file `src/workflows/track-order-placed.ts` with the following content:

```ts title="src/workflows/track-order-placed.ts" highlights={workflowHighlights}
import { 
  createWorkflow,
  transform,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { trackEventStep } from "./steps/track-event"

type WorkflowInput = {
  id: string
}

export const trackOrderPlacedWorkflow = createWorkflow(
  "track-order-placed",
  ({ id }: WorkflowInput) => {
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "id",
        "email",
        "total",
        "currency_code",
        "items.*",
        "customer.id",
        "customer.email",
        "customer.first_name",
        "customer.last_name",
        "created_at",
      ],
      filters: {
        id,
      },
    })

    const order = transform({
      order: orders[0],
    }, ({ order }) => ({
      orderId: order.id,
      email: order.email,
      total: order.total,
      currency: order.currency_code,
      items: order.items?.map((item) => ({
        id: item?.id,
        title: item?.title,
        quantity: item?.quantity,
        variant: item?.variant,
        unit_price: item?.unit_price,
      })),
      customer: {
        id: order.customer?.id,
        email: order.customer?.email,
        firstName: order.customer?.first_name,
        lastName: order.customer?.last_name,
      },
      timestamp: order.created_at,
    }))

    trackEventStep({
      event: "order.placed",
      userId: order.customer?.id,
      properties: order,
    })
  }
)
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case an object holding the ID of the order placed.

In the workflow's constructor function, you:

1. Retrieve the Medusa order using the `useQueryGraphStep` helper step. This step uses Medusa's [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) tool to retrieve data across modules. You pass it the order ID to retrieve.
2. Use [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) to prepare the tracking data, as direct data and variable manipulation isn't allowed in workflows. Learn more in the [Data Manipulation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) documentation.
3. Send the tracking event to Segment using the `trackEventStep` you created in the previous step.

You now have the workflow that tracks the order placement event.

Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) documentation to learn more about workflows and steps.

### c. Handle order.placed Event

Next, you'll create a subscriber that listens to the `order.placed` event and executes the workflow you created in the previous step.

To create the subscriber, create the file `src/subscribers/order-placed.ts` with the following content:

```ts title="src/subscribers/order-placed.ts" highlights={subscriberHighlights}
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { trackOrderPlacedWorkflow } from "../workflows/track-order-placed"

export default async function orderPlacedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await trackOrderPlacedWorkflow(container)
    .run({
      input: {
        id: data.id,
      },
    })
}

export const config: SubscriberConfig = {
  event: "order.placed",
}
```

A subscriber file must export:

1. An asynchronous function, which is the subscriber that is executed when the event is emitted.
2. A configuration object that holds the name of the event that the subscriber listens to, which is `order.placed` in this case.

The subscriber function receives an object as a parameter that has the following properties:

- `event`: An object that holds the event's data payload. The payload of the `order.placed` event is the ID of the order placed.
- `container`: The Medusa container to access the Framework and commerce tools.

In the subscriber function, you execute the `trackOrderPlacedWorkflow` by invoking it, passing the Medusa container as a parameter. Then, you chain a `run` method, passing it the order ID from the event's data payload as input.

Refer to the [Events and Subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) documentation to learn more about creating subscribers.

### Test it Out

You'll now test out the segment integration by placing an order using the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-segment`, you can find the storefront by going back to the parent directory and changing to the `medusa-segment-storefront` directory:

```bash
cd ../medusa-segment-storefront # change based on your project name
```

First, run the following command in your Medusa application's directory to start the Medusa server:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

Then, run the following command in your Next.js Starter Storefront's directory to start the storefront:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

In the storefront, add a product to the cart and proceed to checkout. Once you place the order, open the Segment dashboard to view the order event:

1. Go to Connections > Sources.
2. Click on the Node.js source you created earlier.
3. Click on the "Debugger" tab at the top of the page.
4. You should see the `order.placed` event with the order details.

The event may take a few seconds to appear in the debugger.

![Order Placed Event in Segment Debugger](https://res.cloudinary.com/dza7lstvk/image/upload/v1748259137/Medusa%20Book/CleanShot_2025-05-26_at_14.31.25_2x_xi5p9h.png)

***

## Track Custom Event

In your Medusa application, you often need to track custom events that are relevant to your business use case. For example, a B2B business may want to track whenever a user requests a quote.

In Medusa, you can emit custom events in your workflows when an action occurs. Then, you can create a subscriber that listens to the custom event and executes a workflow to track it in Segment.

For example, if you have a `createQuoteWorkflow`, you can use Medusa's [emitEventStep](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/emit-event#emit-event-in-a-workflow/index.html.md) to emit a custom event after the quote is created:

```ts title="src/workflows/create-quote.ts"
import { 
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import {
  emitEventStep,
} from "@medusajs/medusa/core-flows"

const createQuoteWorkflow = createWorkflow(
  "create-quote",
  () => {
    // ...

    emitEventStep({
      eventName: "quote.created",
      data: {
        id: "123",
        // other data payload
      },
    })
  }
)
```

You can then create a subscriber that listens to the `quote.created` event and executes a workflow to track it in Segment:

```ts title="src/subscribers/quote-created.ts"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { trackQuoteWorkflow } from "../workflows/track-order-placed"

export default async function orderPlacedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await trackQuoteWorkflow(container)
    .run({
      input: {
        id: data.id,
      },
    })
}

export const config: SubscriberConfig = {
  event: "quote.created",
}
```

The above example assumes you have a `trackQuoteWorkflow` that tracks the quote creation event in Segment, similar to the [trackOrderPlacedWorkflow](#b-create-track-order-placed-workflow) you created earlier.

***

## Next Steps

You've now integrated Segment with your Medusa application and tracked common events like order placement. You can expand on the features in this tutorial to:

- Track more events in your Medusa application, such as user sign-ups, cart additions, and more. You can refer to the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for a full list of events emitted by Medusa.
- Emit custom events that are relevant for your business use case, and track them in Segment.
- Add destinations to Segment to benefit from the data collected. Segment supports various destinations, such as Google Analytics, Metabase, and more.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.
3. Contact the [sales team](https://medusajs.com/contact/) to get help from the Medusa team.


# Integrate Sentry (Instrumentation) with Medusa

In this tutorial, you'll learn how to integrate Sentry with your Medusa application to monitor performance and track errors effectively.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. An essential part of building customizations is tracking errors in them and ensuring they have good performance.

Medusa facilitates error tracking and performance monitoring by integrating with instrumentation tools like [Sentry](https://sentry.io/). Sentry provides real-time tracking of errors and performance issues in your application, allowing you to identify and fix them quickly.

In this tutorial, you'll integrate Sentry with Medusa to monitor your application's performance and errors.

## Summary

By following this tutorial, you'll learn how to:

- Install and set up Medusa.
- Set up instrumentation with Sentry. You'll trace HTTP requests, workflows, Query usages, and database operations.
- Capture HTTP request errors in Sentry.
- Test monitoring and error tracking.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Diagram showcasing what is traced in Sentry](https://res.cloudinary.com/dza7lstvk/image/upload/v1750069550/Medusa%20Resources/sentry-integration_xb2ior.jpg)

[Example Repository](https://github.com/medusajs/examples/tree/main/sentry-integration): Find the full code of the guide in this repository.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

First, you'll be asked for the project's name. Then, when prompted about installing the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Set Up Instrumentation with Sentry

Medusa supports instrumentation and reporting through OpenTelemetry, which allows you to integrate with various monitoring tools, including Sentry. This also gives you the flexibility to switch between different monitoring tools without changing your codebase.

In this step, you'll set up instrumentation in your Medusa application using Sentry.

Refer to the [Instrumentation](https://docs.medusajs.com/docs/learn/debugging-and-testing/instrumentation/index.html.md) documentation for more details on how to set up instrumentation in Medusa.

### a. Install Instrumentation Dependencies

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:

```bash npm2yarn
npm install @sentry/node @opentelemetry/api @opentelemetry/exporter-trace-otlp-grpc @sentry/opentelemetry-node @opentelemetry/core@1.x @opentelemetry/sdk-trace-base@1.x @opentelemetry/semantic-conventions@1.x
```

### b. Configure Instrumentation

Now that you have the necessary dependencies installed, you need to configure instrumentation in your Medusa application.

You can configure instrumentation in a special `instrumentation.ts` file at the root of your Medusa application. The Medusa application automatically loads this file when it starts.

You should already have a file named `instrumentation.ts` in your Medusa application with commented-out code. Replace the file's content with the following:

```ts title="instrumentation.ts"
import Sentry from "@sentry/node"
import otelApi from "@opentelemetry/api"
import { registerOtel } from "@medusajs/medusa"
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc" 
import { 
  SentrySpanProcessor, 
  SentryPropagator,
} from "@sentry/opentelemetry-node"

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 1.0,
  // @ts-ignore
  instrumenter: "otel",
})

otelApi.propagation.setGlobalPropagator(new SentryPropagator())

export function register() {
  registerOtel({
    serviceName: "medusa",
    spanProcessors: [new SentrySpanProcessor()],
    traceExporter: new OTLPTraceExporter(),
    instrument: {
      http: true,
      workflows: true,
      query: true,
      db: true,
    },
  })
}
```

You first initialize Sentry with your Data Source Name (DSN), which you'll retrieve and set as an environment variable soon. You also set the `tracesSampleRate` to `1.0`, which enables tracing in your Sentry project.

Next, you set the global propagator to Sentry's propagator, which allows Sentry to propagate trace context across your application.

Finally, you export a `register` function that registers OpenTelemetry. It uses the `registerOtel` function from Medusa, which accepts [OpenTelemetry configurations](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_sdk-node.NodeSDKConfiguration.html) with the following custom Medusa options:

- `serviceName`: The name of your service, which you set to "medusa". You can change it to a more descriptive name if needed.
- `instrument`: An object that specifies what to trace in your Medusa application. You enable tracing for HTTP requests, workflows, Query usages, and database operations by setting their respective properties to `true`.

You can add any additional configurations to either Sentry or OpenTelemetry as needed for your use case.

### c. Set Up a Sentry Project

The final piece to get the Sentry integration working is to set up a Sentry project and get your DSN. The DSN is a unique identifier for your Sentry project that allows your Medusa application to send error and performance data to Sentry.

To set up a Sentry project:

1. [Log in or sign up](https://sentry.io/signup/) to Sentry.
2. In your Sentry dashboard, if you're new to Sentry, click the "Start" button. Otherwise, go to Projects from the sidebar and click the "Create Project" button.
3. In the "Select Platform" step, select "Node.js" as the platform, then select "Node, Vanilla" in the pop-up.

![Screenshot of the pop up with Node, Vanilla selected](https://res.cloudinary.com/dza7lstvk/image/upload/v1750064385/Medusa%20Resources/CleanShot_2025-06-16_at_10.29.29_2x_dkmwtn.png)

4. In the "Configure Node.js SDK" step, you can find your DSN in the second code block. Copy the DSN, as you'll need it in the next step.

![Screenshot of the Configure Node.js SDK step with the DSN highlighted](https://res.cloudinary.com/dza7lstvk/image/upload/v1750064502/Medusa%20Resources/CleanShot_2025-06-16_at_12.00.36_2x_wlc0o1.png)

You can skip onboarding or the next steps for now. You'll test out the integration in the next step.

Then, add the following environment variable to your `.env` file in your Medusa application:

```shell title=".env"
SENTRY_DSN=your_sentry_dsn_here
```

Replace `your_sentry_dsn_here` with the DSN you copied from Sentry.

### Test Tracing in Sentry

You can now test the integration to ensure operations are traced in Sentry.

First, start your Medusa application by running the following command in your Medusa application's directory:

```bash npm2yarn
npm run dev
```

You'll find in the terminal the following message:

```bash
info:    OTEL registered
```

This message indicates that OpenTelemetry has been successfully registered and instrumentation is set up successfully.

Then, open your Medusa Admin dashboard in your browser at `http://localhost:9000/app` and log in with the user you created earlier.

You can already find the database, workflow executions, and HTTP requests traced in your Sentry project:

1. In your Sentry dashboard, go to Explore → Traces from the sidebar.
2. You'll find a list of traces on this page, including HTTP requests made to log in to the Medusa Admin dashboard.

![Sentry dashboard with traces listed](https://res.cloudinary.com/dza7lstvk/image/upload/v1750065824/Medusa%20Resources/CleanShot_2025-06-16_at_12.23.29_2x_at87kq.png)

If you click on a trace, you can see more details about the trace. For example, if you click on an HTTP request trace, you can see the middlewares and workflows executed, the time taken for each operation, the status of the request, and more.

![Sentry trace details with HTTP request and workflow execution](https://res.cloudinary.com/dza7lstvk/image/upload/v1750065922/Medusa%20Resources/CleanShot_2025-06-16_at_12.25.10_2x_w8jzgl.png)

### Test Tracing for Customizations

Tracing also works for your custom API routes, workflow executions, Query usages, and database operations.

In this section, you'll test it by creating a custom API route that throws an error. For now, you'll only view its tracing in Sentry. Later, you'll configure capturing HTTP request errors in Sentry.

An [API Route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) is an endpoint that exposes commerce features to external applications and clients, such as storefronts.

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

So, to create an API route at the path `/test-sentry`, create the file `src/api/test-sentry/route.ts` with the following content:

```ts title="src/api/test-sentry/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  throw new MedusaError(
    MedusaError.Types.UNEXPECTED_STATE,
    "This is a test error for Sentry integration."
  )
}
```

Since you export a `GET` route handler function, you expose a `GET` API route at `/test-sentry`. The route handler function accepts two parameters:

1. A request object with details and context on the request, such as body parameters.
2. A response object to manipulate and send the response.

In the route handler, you throw a `MedusaError` with a custom message. This error will be returned as a response to the request. Later, when you configure error capturing in Sentry, this error will be captured and displayed in the Sentry dashboard.

To test the API route, make sure your Medusa application is running, then go to `http://localhost:9000/test-sentry` in your browser or use `curl`:

```bash
curl http://localhost:9000/test-sentry
```

You'll receive the following error in the response:

```json
{
  "type": "unexpected_state",
  "message": "This is a test error for Sentry integration."
}
```

If you refresh the Tracing page in your Sentry dashboard, you should see a new trace for the `/test-sentry` API route.

It may take a few seconds for the trace to appear in Sentry, so if you don't see it immediately, wait a bit and refresh the page.

![Trace dashboard with the test API route trace](https://res.cloudinary.com/dza7lstvk/image/upload/v1750066376/Medusa%20Resources/CleanShot_2025-06-16_at_12.32.37_2x_uox6r1.png)

If you click on the Trace ID, you'll see the details of the trace.

![Sentry trace details with error breadcrumbs](https://res.cloudinary.com/dza7lstvk/image/upload/v1750066493/Medusa%20Resources/CleanShot_2025-06-16_at_12.34.33_2x_unzktq.png)

***

## Step 3: Capture HTTP Request Errors in Sentry

In the previous step, you tested tracing in Sentry by creating a custom API route that throws an error. However, the error was not captured by Sentry. So, you couldn't see the error details in the Sentry dashboard or get notified about it.

In this step, you'll capture HTTP request errors in Sentry. You'll do that by customizing Medusa's default error handler that is used to handle errors in API routes.

Refer to the [Throwing and Handling Errors](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/errors/index.html.md) guide to learn more about Medusa's default error handling behavior.

To customize the error handler, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" highlights={errorHandlerHighlights}
import { 
  defineMiddlewares, 
  errorHandler, 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
import * as Sentry from "@sentry/node"

const originalErrorHandler = errorHandler()

export default defineMiddlewares({
  errorHandler: (
    error: MedusaError | any, 
    req: MedusaRequest, 
    res: MedusaResponse, 
    next: MedusaNextFunction
  ) => {
    Sentry.captureException(error)
    return originalErrorHandler(error, req, res, next)
  },
})
```

You export the `defineMiddlewares` function that allows you to apply middlewares and customize Medusa's default error handler.

The function accepts an object with the `errorHandler` property, whose value is a custom function that handles errors thrown in API routes.

In the custom error handler, you first capture the error in Sentry using `Sentry.captureException(error)`. This allows Sentry to track the error and its context.

Then, you call the original error handler function that Medusa uses to handle errors in API routes. By using this function, you retain Medusa's default error handling behavior of returning the error response to the client.

### Test Error Capturing in Sentry

To test out error capturing in Sentry, start the Medusa application if it's not already running:

```bash npm2yarn
npm run dev
```

Then, send a `GET` request to the `/test-sentry` API route you created earlier:

```bash
curl http://localhost:9000/test-sentry
```

You'll receive the same error response as before:

```json
{
  "type": "unexpected_state",
  "message": "This is a test error for Sentry integration."
}
```

If you check the tracing page in your Sentry dashboard, you'll see a new trace for the `/test-sentry` API route. Click on it to view the details.

You'll find in the tracing page the thrown error highlighted in red, indicating that it was captured by Sentry.

![Sentry trace details with error message](https://res.cloudinary.com/dza7lstvk/image/upload/v1750347022/Medusa%20Resources/CleanShot_2025-06-19_at_18.29.57_2x_aenvrd.png)

If you click on the error, you can view more details about it, such as its stack trace, how many times it occurred, its breadcrumbs, and more.

![Error details in Sentry](https://res.cloudinary.com/dza7lstvk/image/upload/v1750347022/Medusa%20Resources/CleanShot_2025-06-19_at_18.30.07_2x_oz5y9e.png)

By capturing HTTP request errors in Sentry, you can now monitor and track errors in your Medusa application effectively. You can also set up alerts in Sentry to get notified about critical issues and handle them promptly.

***

## Next Steps

You've now integrated Sentry with your Medusa application. You can continue using Sentry to monitor your application's performance and track errors.

You can also expand on your Sentry integration by enabling more features, such as:

- [Alerts](https://docs.sentry.io/product/alerts/) to get notified about critical issues in your application. You can receive alerts via email, Slack, or other channels.
- [Profiling](https://docs.sentry.io/platforms/javascript/guides/nextjs/profiling/node/) to analyze your application's performance and identify bottlenecks.
- [User feedback](https://docs.sentry.io/product/user-feedback/) to collect feedback in your frontend from users about their experience with your application.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.
3. Contact the [sales team](https://medusajs.com/contact/) to get help from the Medusa team.


# Integrate Medusa with ShipStation (Fulfillment)

In this guide, you'll learn how to integrate Medusa with ShipStation.

Refer your technical team to this guide to integrate ShipStation with your Medusa application. You can then enable it using the Medusa Admin as explained in [this user guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations#manage-fulfillment-providers/index.html.md).

When you install a Medusa application, you get a fully-fledged commerce platform with support for customizations. Medusa's [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md) provides fulfillment-related resources and functionalities in your store, but it delegates the processing and shipment of order fulfillments to providers that you can integrate.

[ShipStation](https://shipstation.com/) is a shipping toolbox that connects all your shipping providers within one platform. By integrating it with Medusa, you can allow customers to choose from different providers like DHL and FedEx and view price rates retrieved from ShipStation. Admin users will also process the order fulfillment using the ShipStation integration.

This guide will teach you how to:

- Install and set up Medusa.
- Set up a ShipStation account.
- Integrate ShipStation as a fulfillment provider in Medusa.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer.

[Example Repository](https://github.com/medusajs/examples/tree/main/shipstation-integration): Find the full code of the guide in this repository.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when you're asked whether you want to install the Next.js Starter Storefront, choose `Y` for yes.

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credential and submit the form.

Afterwards, you can login with the new user and explore the dashboard. The Next.js Starter Storefront is also running at `http://localhost:8000`.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Prepare ShipStation Account

In this step, you'll prepare your ShipStation account before integrating it into Medusa. If you don't have an account, create one [here](https://www.shipstation.com/start-a-free-trial).

### Enable Carriers

To create labels for your shipments, you need to enable carriers. This requires you to enter payment and address details.

To enable carriers:

1. On the Onboard page, in the "Enable carriers & see rates" section, click on the "Enable Carriers" button.

![Scroll down to the Enable carriers & see rates section, and find the "Enable Carriers" button.](https://res.cloudinary.com/dza7lstvk/image/upload/v1734523873/Medusa%20Resources/Screenshot_2024-12-18_at_2.10.54_PM_pmvcfr.png)

2. In the pop-up that opens, click on Continue Setup.

![Click on the green Continue Setup button](https://res.cloudinary.com/dza7lstvk/image/upload/v1734524261/Medusa%20Resources/Screenshot_2024-12-18_at_2.11.47_PM_wsl98i.png)

3. In the next section of the form, you have to enter your payment details and billing address. Once done, click on Continue Setup.
4. After that, click the checkboxes on the Terms of Service section, then click the Finish Setup button.

![Enable the two checkboxes, then click on Finish Setup at the bottom right](https://res.cloudinary.com/dza7lstvk/image/upload/v1734524486/Medusa%20Resources/Screenshot_2024-12-18_at_2.20.12_PM_pkixma.png)

5. Once you're done, you can optionally add funds to your account. If you're not US-based, make sure to disable ParcelGuard insurance. Otherwise, an error will occur while retrieving rates later.

### Add Carriers

You must have at least one carrier (shipping provider) added in your ShipStation account. You'll later provide shipping options for each of these carriers in your Medusa application.

To add carriers:

1. On the Onboard page, in the "Enable carriers & see rates" section, click on the "Add your carrier accounts" link.

![Scroll down to the Enable carriers & see rates section, and find the "Add your carrier accounts" link under the "Enable Carriers" button](https://res.cloudinary.com/dza7lstvk/image/upload/v1734336612/Medusa%20Resources/Screenshot_2024-12-16_at_10.09.08_AM_nqshhg.png)

2. Click on a provider from the pop-up window.

![Click on the provider tiles in the pop-up window](https://res.cloudinary.com/dza7lstvk/image/upload/v1734336826/Medusa%20Resources/Screenshot_2024-12-16_at_10.13.37_AM_og4sdq.png)

Based on the provider you chose, you'll have to enter your account details, then submit the form.

### Activate Shipping API

To integrate ShipStation using their API, you must enable the Shipping API Add-On. To do that:

1. Go to Add-Ons from the navigation bar.
2. Find Shipping API and activate it.

You'll later retrieve your API key.

***

## Step 3: Create ShipStation Module Provider

To integrate third-party services into Medusa, you create a custom module. A module is a re-usable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

Medusa's Fulfillment Module delegates processing fulfillments and shipments to other modules, called module providers. In this step, you'll create a ShipStation Module Provider that implements all functionalities required for fulfillment. In later steps, you'll add into Medusa shipping options for ShipStation, and allow customers to choose it during checkout.

Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).

### Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/shipstation`.

![The directory structure of the Medusa application after adding the module's directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1734338950/Medusa%20Resources/shipstation-dir-overview-1_dlsrbv.jpg)

### Create Service

You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, which is useful if your module defines tables in the database, or connect to a third-party service.

In this section, you'll create the ShipStation Module Provider's service and the methods necessary to handle fulfillment.

Start by creating the file `src/modules/shipstation/service.ts` with the following content:

![The directory structure of the Medusa application after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1734339042/Medusa%20Resources/shipstation-dir-overview-2_cmgvcj.jpg)

```ts title="src/modules/shipstation/service.ts" highlights={serviceHighlights1}
import { AbstractFulfillmentProviderService } from "@medusajs/framework/utils"

export type ShipStationOptions = {
  api_key: string
}

class ShipStationProviderService extends AbstractFulfillmentProviderService {
  static identifier = "shipstation"
  protected options_: ShipStationOptions

  constructor({}, options: ShipStationOptions) {
    super()

    this.options_ = options
  }

  // TODO add methods
}

export default ShipStationProviderService
```

A Fulfillment Module Provider service must extend the `AbstractFulfillmentProviderService` class. You'll implement the abstract methods of this class in the upcoming sections.

The service must have an `identifier` static property, which is a unique identifier for the provider. You set the identifier to `shipstation`.

A module can receive options that are set when you later add the module to Medusa's configurations. These options allow you to safely store secret values outside of your code.

The ShipStation module requires an `api_key` option, indicating your ShipStation's API key. You receive the options as a second parameter of the service's constructor.

### Create Client

To send requests to ShipStation, you'll create a client class that provides the methods to send requests. You'll then use that class in your service.

Create the file `src/modules/shipstation/client.ts` with the following content:

![The directory structure of the Medusa application after adding the client file](https://res.cloudinary.com/dza7lstvk/image/upload/v1734339519/Medusa%20Resources/shipstation-dir-overview-3_b8im2d.jpg)

```ts title="src/modules/shipstation/client.ts" highlights={clientHighlights1}
import { ShipStationOptions } from "./service"
import { MedusaError } from "@medusajs/framework/utils"

export class ShipStationClient {
  options: ShipStationOptions

  constructor(options) {
    this.options = options
  }

  private async sendRequest(url: string, data?: RequestInit): Promise<any> {
    return fetch(`https://api.shipstation.com/v2${url}`, {
      ...data,
      headers: {
        ...data?.headers,
        "api-key": this.options.api_key,
        "Content-Type": "application/json",
      },
    }).then((resp) => {
      const contentType = resp.headers.get("content-type")
      if (!contentType?.includes("application/json")) {
        return resp.text()
      }

      return resp.json()
    })
    .then((resp) => {
      if (typeof resp !== "string" && resp.errors?.length) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          `An error occurred while sending a request to ShipStation: ${
            resp.errors.map((error) => error.message)
          }`
        )
      }

      return resp
    })
  }
}
```

The `ShipStationClient` class accepts the ShipStation options in its constructor and sets those options in the `options` property.

You also add a private `sendRequest` method that accepts a path to send a request to and the request's configurations. In the method, you send a request using the Fetch API, passing the API key from the options in the request header. You also parse the response body based on its content type, and check if there are any errors to be thrown before returning the parsed response.

You'll add more methods to send requests in the upcoming steps.

To use the client in `ShipStationProviderService`, add it as a class property and initialize it in the constructor:

```ts title="src/modules/shipstation/service.ts" highlights={serviceHighlights2}
// imports...
import { ShipStationClient } from "./client"

// ...

class ShipStationProviderService extends AbstractFulfillmentProviderService {
  // properties...
  protected client: ShipStationClient

  constructor({}, options: ShipStationOptions) {
    // ...
    this.client = new ShipStationClient(options)
  }
}
```

You import `ShipStationClient` and add a new `client` property in `ShipStationProviderService`. In the class's constructor, you set the `client` property by initializing `ShipStationProviderService`, passing it the module's options.

You'll use the `client` property when implementing the service's methods.

### Implement Service Methods

In this section, you'll go back to the `ShipStationProviderService` method to implement the abstract methods of `AbstractFulfillmentProviderService`.

Refer to [this guide](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) for a full reference of all methods, their parameters and return types.

#### getFulfillmentOptions

The `getFulfillmentOptions` method returns the options that this fulfillment provider supports. When admin users add shipping options later in the Medusa Admin, they'll select one of these options.

Learn more about shipping options in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/index.html.md).

ShipStation requires that a shipment must be associated with a carrier and one of its services. So, in this method, you'll retrieve the list of carriers from ShipStation and return them as fulfillment options. Shipping options created from these fulfillment options will always have access to the option's carrier and service.

Before you start implementing methods, you'll add the expected carrier types returned by ShipStation. Create the file `src/modules/shipstation/types.ts` with the following content:

![The directory structure of the Medusa application after adding the types file](https://res.cloudinary.com/dza7lstvk/image/upload/v1734340402/Medusa%20Resources/shipstation-dir-overview-4_fwsle0.jpg)

```ts title="src/modules/shipstation/types.ts"
export type Carrier = {
  carrier_id: string
  disabled_by_billing_plan: boolean
  friendly_name: string
  services: {
    service_code: string
    name: string
  }[]
  packages: {
    package_code: string
  }[]
  [k: string]: unknown
}

export type CarriersResponse = {
  carriers: Carrier[]
}
```

You define a `Carrier` type that holds a carrier's details, and a `CarriersResponse` type, which is the response returned by ShipStation.

A carrier has more fields that you can use. Refer to [ShipStation's documentation](https://docs.shipstation.com/openapi/carriers/list_carriers#carriers/list_carriers/t=response\&c=200\&path=carriers) for all carrier fields.

Next, you'll add in `ShipStationClient` the method to retrieve the carriers from ShipStation. So, add to the class defined in `src/modules/shipstation/client.ts` a new method:

```ts title="src/modules/shipstation/client.ts" highlights={clientHighlights2}
// other imports...
import { 
  CarriersResponse,
} from "./types"

export class ShipStationClient {
  // ...
  async getCarriers(): Promise<CarriersResponse> {
    return await this.sendRequest("/carriers") 
  }
}
```

You added a new `getCarriers` method that uses the `sendRequest` method to send a request to the [ShipStation's List Carriers endpoint](https://docs.shipstation.com/openapi/carriers/list_carriers). The method returns `CarriersResponse` that you defined earlier.

Finally, add the `getFulfillmentOptions` method to `ShipStationProviderService`:

```ts title="src/modules/shipstation/service.ts" highlights={serviceHighlights3}
// other imports...
import { 
  FulfillmentOption,
} from "@medusajs/framework/types"

class ShipStationProviderService extends AbstractFulfillmentProviderService {
  // ...
  async getFulfillmentOptions(): Promise<FulfillmentOption[]> {
    const { carriers } = await this.client.getCarriers() 
    const fulfillmentOptions: FulfillmentOption[] = []

    carriers
      .filter((carrier) => !carrier.disabled_by_billing_plan)
      .forEach((carrier) => {
        carrier.services.forEach((service) => {
          fulfillmentOptions.push({
            id: `${carrier.carrier_id}__${service.service_code}`,
            name: service.name,
            carrier_id: carrier.carrier_id,
            carrier_service_code: service.service_code,
          })
        })
      })

    return fulfillmentOptions
  }
}
```

In the `getFulfillmentOptions` method, you retrieve the carriers from ShipStation. You then filter out the carriers disabled by your ShipStation billing plan, and loop over the remaining carriers and their services.

You return an array of fulfillment-option objects, where each object represents a carrier and service pairing. Each object has the following properties:

- an `id` property, which you set to a combination of the carrier ID and the service code.
- a `name` property, which you set to the service's `name`. The admin user will see this name when they create a shipping option for the ShipStation provider.
- You can pass other data, such as `carrier_id` and `carrier_service_code`, and Medusa will store the fulfillment option in the `data` property of shipping options created later.

Learn more about the shipping option's `data` property in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/index.html.md).

You'll see this method in action later when you create a shipping option.

#### canCalculate

When an admin user creates a shipping option for your provider, they can choose whether the price is flat rate or calculated during checkout.

If the user chooses calculated, Medusa validates that your fulfillment provider supports calculated prices using the `canCalculate` method of your provider's service.

This method accepts the shipping option's `data` field, which will hold the data of an option returned by `getFulfillmentOptions`. It returns a boolean value indicating whether the shipping option can have a calculated price.

Add the method to `ShipStationProviderService` in `src/modules/shipstation/service.ts`:

```ts title="src/modules/shipstation/service.ts"
// other imports...
import {
  // ...
  CreateShippingOptionDTO,
} from "@medusajs/framework/types"

class ShipStationProviderService extends AbstractFulfillmentProviderService {
  // ...
  async canCalculate(data: CreateShippingOptionDTO): Promise<boolean> {
    return true
  }
}
```

Since all shipping option prices can be calculated with ShipStation based on the chosen carrier and service zone, you always return `true` in this method.

You'll implement the calculation mechanism in a later method.

#### calculatePrice

When the customer views available shipping options during checkout, the Medusa application requests the calculated price from your fulfillment provider using its `calculatePrice` method.

To retrieve shipping prices with ShipStation, you create a shipment first then get its rates. So, in the `calculatePrice` method, you'll either:

- Send a request to [ShipStation's get shipping rates endpoint](https://docs.shipstation.com/openapi/rates/calculate_rates) that creates a shipment and returns its prices;
- Or, if a shipment was already created before, you'll retrieve its prices using [ShipStation's get shipment rates endpoint](https://docs.shipstation.com/openapi/shipments/list_shipment_rates).

First, add the following types to `src/modules/shipstation/types.ts`:

```ts title="src/modules/shipstation/types.ts" highlights={typesHighlights1}
export type ShipStationAddress = {
  name: string
  phone: string
  email?: string | null
  company_name?: string | null
  address_line1: string
  address_line2?: string | null
  address_line3?: string | null
  city_locality: string
  state_province: string
  postal_code: string
  country_code: string
  address_residential_indicator: "unknown" | "yes" | "no"
  instructions?: string | null
  geolocation?: {
    type?: string
    value?: string
  }[]
}

export type Rate = {
  rate_id: string
  shipping_amount: {
    currency: string
    amount: number
  }
  insurance_amount: {
    currency: string
    amount: number
  }
  confirmation_amount: {
    currency: string
    amount: number
  }
  other_amount: {
    currency: string
    amount: number
  }
  tax_amount: {
    currency: string
    amount: number
  }
}

export type RateResponse = {
  rates: Rate[]
}

export type GetShippingRatesRequest = {
  shipment_id?: string
  shipment?: Omit<Shipment, "shipment_id" | "shipment_status">
  rate_options: {
    carrier_ids: string[]
    service_codes: string[]
    preferred_currency: string
  }
}

export type GetShippingRatesResponse = {
  shipment_id: string
  carrier_id?: string
  service_code?: string
  external_order_id?: string
  rate_response: RateResponse
}

export type Shipment = {
  shipment_id: string
  carrier_id: string
  service_code: string
  ship_to: ShipStationAddress
  return_to?: ShipStationAddress
  is_return?: boolean
  ship_from: ShipStationAddress
  items?: [
    {
      name?: string
      quantity?: number
      sku?: string
    }
  ]
  warehouse_id?: string
  shipment_status: "pending" | "processing" | "label_purchased" | "cancelled"
  [k: string]: unknown
}

```

You add the following types:

- `ShipStationAddress`: an address to ship from or to.
- `Rate`: a price rate for a specified carrier and service zone.
- `RateResponse`: The response when retrieving rates.
- `GetShippingRatesRequest`: The request body data for [ShipStation's get shipping rates endpoint](https://docs.shipstation.com/openapi/rates/calculate_rates). You can refer to their API reference for other accepted parameters.
- `GetShippingRatesResponse`: The response of the [ShipStation's get shipping rates endpoint](https://docs.shipstation.com/openapi/rates/calculate_rates). You can refer to their API reference for other response fields.
- `Shipment`: A shipment's details.

Then, add the following methods to `ShipStationClient`:

```ts title="src/modules/shipstation/client.ts" highlights={serviceHighlights7}
// other imports...
import { 
  // ...
  GetShippingRatesRequest,
  GetShippingRatesResponse,
  RateResponse,
} from "./types"

export class ShipStationClient {
  // ...
  async getShippingRates(
    data: GetShippingRatesRequest
  ): Promise<GetShippingRatesResponse> {
    return await this.sendRequest("/rates", {
      method: "POST",
      body: JSON.stringify(data),
    }).then((resp) => {
      if (resp.rate_response.errors?.length) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          `An error occurred while retrieving rates from ShipStation: ${
            resp.rate_response.errors.map((error) => error.message)
          }`
        )
      }

      return resp
    })
  }

  async getShipmentRates(id: string): Promise<RateResponse[]> {
    return await this.sendRequest(`/shipments/${id}/rates`)
  }
}
```

The `getShippingRates` method accepts as a parameter the data to create a shipment and retrieve its rate. In the method, you send the request using the `sendRequest` method, and throw any errors in the rate retrieval before returning the response.

The `getShipmentRates` method accepts the ID of the shipment as a parameter, sends the request using the `sendRequest` method and returns its response holding the shipment's rates.

Next, add to `ShipStationProviderService` a private method that'll be used to create a shipment in ShipStation and get its rates:

```ts title="src/modules/shipstation/service.ts" highlights={serviceHighlights8}
// other imports...
import {
  // ...
  MedusaError,
} from "@medusajs/framework/utils"
import { 
  // ...
  CalculateShippingOptionPriceDTO,
} from "@medusajs/framework/types"
import {
  GetShippingRatesResponse,
  ShipStationAddress,
} from "./types"

class ShipStationProviderService extends AbstractFulfillmentProviderService {
  // ...
  private async createShipment({
    carrier_id,
    carrier_service_code,
    from_address,
    to_address,
    items,
    currency_code,
  }: {
    carrier_id: string
    carrier_service_code: string
    from_address?: {
      name?: string
      address?: Omit<
        StockLocationAddressDTO, "created_at" | "updated_at" | "deleted_at"
      >
    },
    to_address?: Omit<
      CartAddressDTO, "created_at" | "updated_at" | "deleted_at" | "id"
    >,
    items: CartLineItemDTO[] | OrderLineItemDTO[],
    currency_code: string
  }): Promise<GetShippingRatesResponse> {
    if (!from_address?.address) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "from_location.address is required to calculate shipping rate"
      )
    }
    const ship_from: ShipStationAddress = {
      name: from_address?.name || "",
      phone: from_address?.address?.phone || "",
      address_line1: from_address?.address?.address_1 || "",
      city_locality: from_address?.address?.city || "",
      state_province: from_address?.address?.province || "",
      postal_code: from_address?.address?.postal_code || "",
      country_code: from_address?.address?.country_code || "",
      address_residential_indicator: "unknown",
    }
    if (!to_address) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "shipping_address is required to calculate shipping rate"
      )
    }
    
    const ship_to: ShipStationAddress = {
      name: `${to_address.first_name} ${to_address.last_name}`,
      phone: to_address.phone || "",
      address_line1: to_address.address_1 || "",
      city_locality: to_address.city || "",
      state_province: to_address.province || "",
      postal_code: to_address.postal_code || "",
      country_code: to_address.country_code || "",
      address_residential_indicator: "unknown",
    }

    // TODO create shipment
  }
}
```

The `createShipment` method accepts as a parameter an object having the following properties:

- `carrier_id`: The ID of the carrier to create the shipment for.
- `carrier_service_code`: The code of the carrier's service.
- `from_address`: The address to ship items from, which is the address of the stock location associated with a shipping option.
- `to_address`: The address to ship items to, which is the customer's address.
- `items`: An array of the items in the cart or order (for fulfilling the order later).
- `currency_code`: The currency code of the cart or order.

In the `createShipment` method, so far you only prepare the data to be sent to ShipStation. ShipStation requires the addresses to ship the items from and to.

To send the request, replace the `TODO` with the following:

```ts title="src/modules/shipstation/service.ts"
// Sum the package's weight
// You can instead create different packages for each item
const packageWeight = items.reduce((sum, item) => {
  // @ts-ignore
  return sum + (item.variant.weight || 0)
}, 0)

return await this.client.getShippingRates({
  shipment: {
    carrier_id: carrier_id,
    service_code: carrier_service_code,
    ship_to,
    ship_from,
    validate_address: "no_validation",
    items: items?.map((item) => ({
      name: item.title,
      quantity: item.quantity,
      sku: item.variant_sku || "",
    })),
    packages: [{
      weight: {
        value: packageWeight,
        unit: "kilogram",
      },
    }],
    customs: {
      contents: "merchandise",
      non_delivery: "return_to_sender",
    },
  },
  rate_options: {
    carrier_ids: [carrier_id],
    service_codes: [carrier_service_code],
    preferred_currency: currency_code as string,
  },
})
```

You create a shipment and get its rates using the `getShippingRates` method you added to the client. You pass the method the expected request body parameters by [ShipStation's get shipping rates endpoint](https://docs.shipstation.com/openapi/rates/calculate_rates), including the carrier ID, the items to be shipped, and more.

The above snippet assumes all items are sent in a single package. You can instead pass a package for each item, specifying its weight and optionally its height, width, and length.

Finally, add the `calculatePrice` method to `ShipStationProviderService`:

```ts title="src/modules/shipstation/service.ts" highlights={serviceHighlights5}
// other imports...
import { 
  // ...
  CalculatedShippingOptionPrice,
} from "@medusajs/framework/types"

class ShipStationProviderService extends AbstractFulfillmentProviderService {
  // ...
  async calculatePrice(
    optionData: CalculateShippingOptionPriceDTO["optionData"], 
    data: CalculateShippingOptionPriceDTO["data"], 
    context: CalculateShippingOptionPriceDTO["context"]
  ): Promise<CalculatedShippingOptionPrice> {
    const { shipment_id } = data as {
      shipment_id?: string
    } || {}
    const { carrier_id, carrier_service_code } = optionData as {
      carrier_id: string
      carrier_service_code: string
    }
    let rate: Rate | undefined

    if (!shipment_id) {
      const shipment = await this.createShipment({
        carrier_id,
        carrier_service_code,
        from_address: {
          name: context.from_location?.name,
          address: context.from_location?.address,
        },
        to_address: context.shipping_address,
        items: context.items || [],
        currency_code: context.currency_code as string,
      })
      rate = shipment.rate_response.rates[0]
    } else {
      const rateResponse = await this.client.getShipmentRates(shipment_id)
      rate = rateResponse[0].rates[0]
    }

    const calculatedPrice = !rate ? 0 : rate.shipping_amount.amount + rate.insurance_amount.amount + 
      rate.confirmation_amount.amount + rate.other_amount.amount + 
      (rate.tax_amount?.amount || 0)

    return {
      calculated_amount: calculatedPrice,
      is_calculated_price_tax_inclusive: !!rate?.tax_amount,
    }
  }
}
```

The `calculatePrice` method accepts the following parameters:

1. The `data` property of the chosen shipping option during checkout.
2. The `data` property of the shipping method, which will hold the ID of the shipment in ShipStation.
3. An object of the checkout's context, including the cart's items, the location associated with the shipping option, and more.

In the method, you first check if a `shipment_id` is already stored in the shipping method's `data` property. If so, you retrieve the shipment's rates using the client's `getShipmentRates` method. Otherwise, you use the `createShipment` method to create the shipment and get its rates.

A rate returned by ShipStation has four properties that, when added up, make up the full price: `shipping_amount`, `insurance_amount`, `confirmation_amount`, and `other_amount`. It may have a `tax_amount` property, which is the amount for applied taxes.

Learn more about these fields in [ShipStation's documentation](https://docs.shipstation.com/rate-shopping#about-the-response).

The method returns an object having the following properties:

- `calculated_amount`: The shipping method's price calculated by adding the four rate properties with the tax property, if available.
- `is_calculated_price_tax_inclusive`: Whether the price includes taxes, which is inferred from whether the `tax_amount` property is set in the rate.

Customers will now see the calculated price of a ShipStation shipping option during checkout.

#### validateFulfillmentData

When a customer chooses a shipping option during checkout, Medusa creates a shipping method from that option. A shipping method has a `data` property to store data relevant for later processing of the method and its fulfillments.

So, in the `validateFulfillmentData` method of your provider, you'll create a shipment in ShipStation if it wasn't already created using their [get shipping rates endpoint](https://docs.shipstation.com/openapi/rates/calculate_rates), and store the ID of that shipment in the created shipping method's `data` property.

Add the `validateFulfillmentData` method to `ShipStationProviderService`:

```ts title="src/modules/shipstation/service.ts" highlights={serviceHighlights4}
class ShipStationProviderService extends AbstractFulfillmentProviderService {
  // ...
  async validateFulfillmentData(
    optionData: Record<string, unknown>, 
    data: Record<string, unknown>, 
    context: Record<string, unknown>
  ): Promise<any> {
    let { shipment_id } = data as {
      shipment_id?: string
    }

    if (!shipment_id) {
      const { carrier_id, carrier_service_code } = optionData as {
        carrier_id: string
        carrier_service_code: string
      }
      const shipment = await this.createShipment({
        carrier_id,
        carrier_service_code,
        from_address: {
          // @ts-ignore
          name: context.from_location?.name,
          // @ts-ignore
          address: context.from_location?.address,
        },
        // @ts-ignore
        to_address: context.shipping_address,
        // @ts-ignore
        items: context.items || [],
        // @ts-ignore
        currency_code: context.currency_code,
      })
      shipment_id = shipment.shipment_id
    }

    return {
      ...data,
      shipment_id,
    }
  }
}
```

The `validateFulfillmentData` method accepts the following parameters:

1. The `data` property of the chosen shipping option during checkout. It will hold the carrier ID and its service code.
2. The `data` property of the shipping method to be created. This can hold custom data sent in the [Add Shipping Method API route](https://docs.medusajs.com/api/store#carts_postcartsidshippingmethods).
3. An object of the checkout's context, including the cart's items, the location associated with the shipping option, and more.

In the method, you try to retrieve the shipment ID from the shipping method's `data` parameter if it was already created. If not, you create the shipment in ShipStation using the `createShipment` method.

Finally, you return the object to be stored in the shipping method's `data` property. You include in it the ID of the shipment in ShipStation.

#### createFulfillment

After the customer places the order, the admin user can manage its fulfillments. When the admin user creates a fulfillment for the order, Medusa uses the `createFulfillment` method of the associated provider to handle any processing in the third-party provider.

This method supports creating split fulfillments, meaning you can partially fulfill and order's items. So, you'll create a new shipment, then purchase a label for that shipment. You'll use the existing shipment to retrieve details like the address to ship from and to.

First, add a new type to `src/modules/shipstation/types.ts`:

```ts title="src/modules/shipstation/types.ts"
export type Label = {
  label_id: string
  status: "processing" | "completed" | "error" | "voided"
  shipment_id: string
  ship_date: Date
  shipment_cost: {
    currency: string
    amount: number
  }
  insurance_cost: {
    currency: string
    amount: number
  }
  confirmation_amount: {
    currency: string
    amount: number
  }
  tracking_number: string
  is_return_label: boolean
  carrier_id: string
  service_code: string
  trackable: string
  tracking_status: "unknown" | "in_transit" | "error" | "delivered"
  label_download: {
    href: string
    pdf: string
    png: string
    zpl: string
  }
}
```

You add the `Label` type for the details in a label object. You can find more properties in [ShipStation's documentation](https://docs.shipstation.com/openapi/labels/create_label#labels/create_label/response\&c=200/body).

Then, add the following methods to the `ShipStationClient`:

```ts title="src/modules/shipstation/client.ts"
// other imports...
import { 
  // ...
  Label,
  Shipment,
} from "./types"

export class ShipStationClient {
  // ...

  async getShipment(id: string): Promise<Shipment> {
    return await this.sendRequest(`/shipments/${id}`)
  }

  async purchaseLabelForShipment(id: string): Promise<Label> {
    return await this.sendRequest(`/labels/shipment/${id}`, {
      method: "POST",
      body: JSON.stringify({}),
    })
  }
}
```

You add the `getShipment` method to retrieve a shipment's details, and the `purchaseLabelForShipment` method to purchase a label in ShipStation for a shipment by its ID.

Finally, add the `createFulfillment` method in `ShipStationProviderService`:

```ts title="src/modules/shipstation/service.ts" highlights={serviceHighlights6}
class ShipStationProviderService extends AbstractFulfillmentProviderService {
  // ...
  async createFulfillment(
    data: object, 
    items: object[], 
    order: object | undefined, 
    fulfillment: Record<string, unknown>
  ): Promise<any> {
    const { shipment_id } = data as {
      shipment_id: string
    }

    const originalShipment = await this.client.getShipment(shipment_id)

    const orderItemsToFulfill = []

    items.map((item) => {
      // @ts-ignore
      const orderItem = order.items.find((i) => i.id === item.line_item_id)

      if (!orderItem) {
        return
      }

      // @ts-ignore
      orderItemsToFulfill.push({
        ...orderItem,
        // @ts-ignore
        quantity: item.quantity,
      })
    })

    const newShipment = await this.createShipment({
      carrier_id: originalShipment.carrier_id,
      carrier_service_code: originalShipment.service_code,
      from_address: {
        name: originalShipment.ship_from.name,
        address: {
          ...originalShipment.ship_from,
          address_1: originalShipment.ship_from.address_line1,
          city: originalShipment.ship_from.city_locality,
          province: originalShipment.ship_from.state_province,
        },
      },
      to_address: {
        ...originalShipment.ship_to,
        address_1: originalShipment.ship_to.address_line1,
        city: originalShipment.ship_to.city_locality,
        province: originalShipment.ship_to.state_province,
      },
      items: orderItemsToFulfill as OrderLineItemDTO[],
      // @ts-ignore
      currency_code: order.currency_code,
    })

    const label = await this.client.purchaseLabelForShipment(newShipment.shipment_id)

    return {
      data: {
        ...(fulfillment.data as object || {}),
        label_id: label.label_id,
        shipment_id: label.shipment_id,
      },
    }
  }
}
```

This method accepts the following parameters:

- `data`: The `data` property of the associated shipping method, which holds the ID of the shipment.
- `items`: The items to fulfill.
- `order`: The order's details.
- `fulfillment`: The details of the fulfillment to be created.

In the method, you:

- Retrieve the details of the shipment originally associated with the fulfillment's shipping method.
- Filter out the order items to retrieve the items to fulfill.
- Create a new shipment for the items to fulfill. You use the original shipment for details like the carrier ID or the addresses to ship from and to.
- Purchase a label for the new shipment.

You return an object whose `data` property will be stored in the created fulfillment's `data` property. You store in it the ID of the purchased label and the ID of its associated shipment.

#### cancelFulfillment

The last method you'll implement is the `cancelFulfillment` method. When an admin user cancels a fulfillment, Medusa uses the associated provider's `cancelFulfillment` method to perform any necessary actions in the third-party provider.

You'll use this method to void the label in ShipStation that was purchased in the `createFulfillment` method and cancel its associated shipment.

Start by adding the following type to `src/modules/shipstation/types.ts`:

```ts title="src/modules/shipstation/types.ts"
export type VoidLabelResponse = {
  approved: boolean
  message: string
  reason_code?: string
}
```

`VoidLabelResponse` is the response type of [ShipStation's void label endpoint](https://docs.shipstation.com/openapi/labels/void_label).

Next, add two methods to `ShipStationClient`:

```ts title="src/modules/shipstation/client.ts" highlights={clientHighlights4}
// other imports...
import { 
  // ...
  VoidLabelResponse,
} from "./types"

export class ShipStationClient {
  // ...
  async voidLabel(id: string): Promise<VoidLabelResponse> {
    return await this.sendRequest(`/labels/${id}/void`, {
      method: "PUT",
    })
  }

  async cancelShipment(id: string): Promise<void> {
    return await this.sendRequest(`/shipments/${id}/cancel`, {
      method: "PUT",
    })
  }
}
```

You added two methods:

- `voidLabel` that accepts the ID of a label to void using [ShipStation's endpoint](https://docs.shipstation.com/openapi/labels/void_label).
- `cancelShipment` that accepts the ID of a shipment to cancel using [ShipStation's endpoint](https://docs.shipstation.com/openapi/shipments/cancel_shipments).

Finally, in `ShipStationProviderService`, add the `cancelFulfillment` method:

```ts title="src/modules/shipstation/service.ts"
class ShipStationProviderService extends AbstractFulfillmentProviderService {
  // ...
  async cancelFulfillment(data: Record<string, unknown>): Promise<any> {
    const { label_id, shipment_id } = data as {
      label_id: string
      shipment_id: string
    }

    await this.client.voidLabel(label_id)
    await this.client.cancelShipment(shipment_id)
  }
}
```

This method accepts the fulfillment's `data` property as a parameter. You get the ID of the label and shipment from the `data` parameter.

Then, you use the client's `voidLabel` method to void the label, and `cancelShipment` to cancel the shipment.

Refer to [this guide](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) for a full reference of all methods, their parameters and return types.

### Export Module Definition

The `ShipStationProviderService` class now has the methods necessary to handle fulfillments.

Next, you must export the module provider's definition, which lets Medusa know what module this provider belongs to and its service.

Create the file `src/modules/shipstation/index.ts` with the following content:

![The directory structure of the Medusa application after adding the index file](https://res.cloudinary.com/dza7lstvk/image/upload/v1734350125/Medusa%20Resources/shipstation-dir-overview-5_zs6beg.jpg)

```ts title="src/modules/shipstation/index.ts"
import ShipStationProviderService from "./service"
import { 
  ModuleProvider, 
  Modules,
} from "@medusajs/framework/utils"

export default ModuleProvider(Modules.FULFILLMENT, {
  services: [ShipStationProviderService],
})
```

You export the module provider's definition using `ModuleProvider` from the Modules SDK. It accepts as a first parameter the name of the module that this provider belongs to, which is the Fulfillment Module. It also accepts as a second parameter an object having a `service` property indicating the provider's service.

### Add Module to Configurations

Finally, to register modules and module providers in Medusa, you must add them to Medusa's configurations.

Medusa's configurations are set in the `medusa-config.ts` file, which is at the root directory of your Medusa application. The configuration object accepts a `modules` array, whose value is an array of modules to add to the application.

Add the `modules` property to the exported configurations in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/fulfillment",
      options: {
        providers: [
          // default provider
          {
            resolve: "@medusajs/medusa/fulfillment-manual",
            id: "manual",
          },
          {
            resolve: "./src/modules/shipstation",
            id: "shipstation",
            options: {
              api_key: process.env.SHIPSTATION_API_KEY,
            },
          },
        ],
      },
    },
  ],
})
```

In the `modules` array, you pass a module object having the following properties:

- `resolve`: The NPM package of the Fulfillment Module. Since the ShipStation Module is a Fulfillment Module Provider, it'll be passed in the options of the Fulfillment Module.
- `options`: An object of options to pass to the module. It has a `providers` property which is an array of module providers to register. Each module provider object has the following properties:
  - `resolve`: The path to the module provider to register in the application. It can also be the name of an NPM package.
  - `id`: A unique ID, which Medusa will use along with the `identifier` static property that you set earlier in the class to identify this module provider.
  - `options`: An object of options to pass to the module provider. These are the options you expect and use in the module provider's service.

The values of the ShipStation Module's options are set in environment variables. So, add the following environment variables to `.env`:

```shell
SHIPSTATION_API_KEY=123...
```

Where `SHIPSTATION_API_KEY` is the ShipStation API key, which you can retrieve on the ShipStation dashboard:

- Click on the cog icon in the navigation bar to go to Settings.

![The cog icon is at the top right of the navigation bar. It's the third icon from the right.](https://res.cloudinary.com/dza7lstvk/image/upload/v1734352047/Medusa%20Resources/Screenshot_2024-12-16_at_2.27.02_PM_nnmwzo.png)

- In the sidebar, expand Account and click on API Settings

![The sidebar has an Account expandable. When you click on it, more items will show. Click on the API Settings](https://res.cloudinary.com/dza7lstvk/image/upload/v1734352145/Medusa%20Resources/Screenshot_2024-12-16_at_2.28.32_PM_wwfc1s.png).

- On the API Settings page, make sure V2 API is selected for "Select API Version" field, then click the "Generate API Key" button.

![Make sure V2 API is selected in the Select API Version dropdown, then click on the "Generate API Key" button.](https://res.cloudinary.com/dza7lstvk/image/upload/v1734352261/Medusa%20Resources/Screenshot_2024-12-16_at_2.30.31_PM_vbkz4i.png)

- Copy the generated API key and use it as the value of the `SHIPSTATION_API_KEY` environment variable.

***

## Step 4: Add Shipping Options for ShipStation

Now that you've integrated ShipStation, you need to create its shipping options so that customers can choose from them during checkout.

First, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then:

1. Open the Medusa Admin at `http://localhost:9000/app` and log in.
2. Go to Settings -> Locations & Shipping

![After clicking on settings in the main dashboard, a new sidebar will be shown where you can click on Location & Shipping](https://res.cloudinary.com/dza7lstvk/image/upload/v1733923761/Medusa%20Resources/Screenshot_2024-12-11_at_2.41.25_PM_wjbq5f.png)

3. Each location has shipping options. So, either create a new location, or click on the "View details" link at the top-right of a location.

![A location's card has the "View details" link at the top-right.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733923793/Medusa%20Resources/Screenshot_2024-12-11_at_2.41.50_PM_idglsu.png)

4. On the location's page and under the Fulfillment Providers section, click on the three-dots icon and choose Edit from the dropdown.

![The location's page has as a "Fulfillment Providers" section in the side column at the right. Click on the three-dots icon in that section and choose Edit from the dropdown](https://res.cloudinary.com/dza7lstvk/image/upload/v1733923832/Medusa%20Resources/Screenshot_2024-12-11_at_2.42.47_PM_rzbjbf.png)

5. A pop up will open with the list of all integrated fulfillment providers. Click the checkbox at the left of the "Shipstation" provider, then click Save.

![Choose the fulfillment provider from the list and click on the Save button](https://res.cloudinary.com/dza7lstvk/image/upload/v1734517015/Medusa%20Resources/Screenshot_2024-12-18_at_12.06.05_PM_nkmljy.png)

6. Under the Shipping section, click on the "Create option" link.

![The Create Option link is in the Shipping section next to shipping options](https://res.cloudinary.com/dza7lstvk/image/upload/v1734517185/Medusa%20Resources/Screenshot_2024-12-18_at_12.19.26_PM_o3yz4n.png)

7. In the form that opens:
   - Select Calculated for the price type.
   - Enter a name for the shipping option. This is the name that customers see in the storefront.
   - Choose a Shipping Profile.
   - Choose ShipStation for Fulfillment Provider
   - This will load in the "Fulfillment option" field the ShipStation provider's options, which are retrieved on the server from the provider's `getFulfillmentOptions` method. Once they're loaded, choose one of the options retrieved from ShipStation.
   - Click the Save button.

![Select Calculated for price type, and select the correct fulfillment provider and option](https://res.cloudinary.com/dza7lstvk/image/upload/v1734517536/Medusa%20Resources/Screenshot_2024-12-18_at_12.24.44_PM_yk7s3z.png)

You can create a shipping option for each fulfillment option.

Customers can now select this shipping option during checkout, and the fulfillment for their order will be processed by ShipStation.

***

## Test it Out: Place an Order and Fulfill It

To test out the integration, you'll place an order using the Next.js Starter Storefront you installed with the Medusa application. You'll then create a fulfillment for the order's items from the Medusa Admin dashboard.

### Place Order in Storefront

Open the terminal in the Next.js Starter Storefront's directory. It's a sibling directory of the Medusa application with the name `{project-name}-storefront`, where `{project-name}` is the name of the Medusa application's project.

Then, while the Medusa application is running, run the following command in the storefront's directory:

```bash npm2yarn
npm run dev
```

This will run the storefront at `http://localhost:8000`. Open it in your browser, then:

1. Click on Menu at the top left of the navigation bar, then choose Store.

![After you click on Menu, choose Store from the side menu.](https://res.cloudinary.com/dza7lstvk/image/upload/v1734518126/Medusa%20Resources/Screenshot_2024-12-18_at_12.35.10_PM_knk46m.png)

2. Click on a product and add it to the cart.

![On a product's page, choose and option then click "add to cart"](https://res.cloudinary.com/dza7lstvk/image/upload/v1734518298/Medusa%20Resources/Screenshot_2024-12-18_at_12.36.18_PM_lrqnsj.png)

3. Click on "Cart" at the top right to go to the cart's page.

![Click on Cart at the top right of the navigation bar.](https://res.cloudinary.com/dza7lstvk/image/upload/v1734518298/Medusa%20Resources/Screenshot_2024-12-18_at_12.36.59_PM_hmvpgb.png)

4. From the cart's page, click on "Go to checkout".

![The "Go to checkout" button is at the bottom right of the page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1734518298/Medusa%20Resources/Screenshot_2024-12-18_at_12.37.52_PM_ma4dij.png)

5. Enter the customer address as a first step of the Checkout. Make sure that the country you choose is the same as the location that the fulfillment provider's options are available in.

If you're entering US-based address, make sure to enter the two-letter code for your state, as that's required by ShipStation.

6. In the Delivery step, you'll find the option you added for ShipStation. There will be a loading indicator while its price is fetched, and the price will be shown afterwards.

![Price is shown next to the ShipStation shipping option](https://res.cloudinary.com/dza7lstvk/image/upload/v1734528106/Medusa%20Resources/Screenshot_2024-12-18_at_3.19.14_PM_smrzae.png)

7. Click on the ShipStation option, then click Continue to Payment.
8. Finish the payment step, then click Place order in the Review section

You've now created an order that uses a shipping option from ShipStation.

### Fulfill Order in Admin

You'll now manage the order you've created in the admin to fulfill it:

1. Open the admin at `http://localhost:9000/app` and login.
2. You'll find on the Orders page the order you've created. Click on it to view its details.
3. On the order's details page, scroll down to the Unfulfilled Items section. Then, click on the three-dots icon at the top right of the section and choose "Fulfill items" from the dropdown.

![In the Unfulfilled Items section, click on the three dots, then Fulfill items from the dropdown](https://res.cloudinary.com/dza7lstvk/image/upload/v1734530779/Medusa%20Resources/Screenshot_2024-12-18_at_4.05.29_PM_z9lwsk.png)

4. In the form that opens, choose the Location to fulfill the item(s) from, then click Create Fulfillment.

![Choose from the dropdown the location to fulfill the items from, then click Create Fulfillment at the bottom right.](https://res.cloudinary.com/dza7lstvk/image/upload/v1734530888/Medusa%20Resources/Screenshot_2024-12-18_at_4.07.35_PM_f4h5o1.png)

5. The created fulfillment will be showing on the order's details page now.

![Created fulfillment with details](https://res.cloudinary.com/dza7lstvk/image/upload/v1734535715/Medusa%20Resources/Screenshot_2024-12-18_at_5.28.06_PM_tsuk4a.png)

You can also cancel the fulfillment by clicking on the three-dots icon, then choosing Cancel from the dropdown. This will void the label in ShipStation and cancel its shipment.

***

## Next Steps

You've now integrated Medusa with ShipStation. You can fulfill orders with many carriers and providers, all from a single integration and platform.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Integrate Slack (Notification) with Medusa

In this tutorial, you'll learn how to integrate Slack with Medusa to receive notifications about created orders.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. Medusa's architecture facilitates integrating third-party services to customize Medusa's infrastructure for your business needs.

Medusa's [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) allows you to customize Medusa's infrastructure to send notifications using the third-party provider that fits your business needs, such as [Slack](https://slack.com/).

In this tutorial, you'll integrate Slack with Medusa to receive notifications about created orders.

## Summary

By following this tutorial, you'll learn how to:

- Install and set up Medusa.
- Integrate Slack with Medusa.
- Handle Medusa's `order.placed` event to send notifications to Slack.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Slack integration workflow diagram showing the complete integration flow: when ecommerce events occur in Medusa (such as order placement), the system automatically sends webhook notifications to Slack channels, enabling real-time team communication and order monitoring for business operations](https://res.cloudinary.com/dza7lstvk/image/upload/v1748943447/slack-integration-overview_vkdijx.jpg)

[Example Repository](https://github.com/medusajs/examples/tree/main/slack-integration): Find the full code of the guide in this repository.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

First, you'll be asked for the project's name. Then, when prompted about installing the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Slack Module Provider

To integrate third-party services into Medusa, you create a custom [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a reusable package with functionalities related to a single feature or domain.

Medusa's [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) provides an interface to send notifications in your Medusa application. It delegates the actual sending of notifications to the underlying provider, such as Slack.

In this step, you'll integrate Slack as a Notification Module Provider. Later, you'll use it to send a notification when an order is created.

Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules in Medusa.

### a. Install Axios

To send requests to Slack, you'll use the [Axios](https://axios-http.com/) library. So, run the following command to install it in your Medusa application:

```bash npm2yarn
npm install axios
```

You'll use Axios in the module's service.

### b. Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/slack`.

### c. Create Slack Module's Service

A module has a service that contains its logic. For Notification Module Providers, the service implements the logic to send notifications with a third-party service.

To create the service of the Slack Notification Module Provider, create the file `src/modules/slack/service.ts` with the following content:

```ts title="src/modules/slack/service.ts" highlights={serviceHighlights}
import { 
  AbstractNotificationProviderService,
} from "@medusajs/framework/utils"

type Options = {
  webhook_url: string
  admin_url: string
}

type InjectedDependencies = {}

class SlackNotificationProviderService extends AbstractNotificationProviderService {
  static identifier = "slack"
  protected options: Options

  constructor(container: InjectedDependencies, options: Options) {
    super()
    this.options = options
  }
}
```

A Notification Module Provider's service must extend the `AbstractNotificationProviderService` class. You'll implement its methods in a bit.

The service must also have an `identifier` static property, which is a unique identifier for the module. This identifier is used when registering the module in the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

The service's constructor receives two parameters:

- `container`: The [module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) that contains Framework resources available to the module. You don't need to access any resources for this provider.
- `options`: Options that are passed to the module provider when it's registered in Medusa's configurations. You define the following options:
  - `webhook_url`: The Slack webhook URL to send notifications to.
  - `admin_url`: The URL of the Medusa Admin dashboard, which you'll use to add links in the notifications.

You'll learn how to set these options when you [add the module provider to Medusa's configurations](#g-add-module-provider-to-medusas-configurations).

In the constructor, you set the `options` property to the passed options.

In the next sections, you'll implement the methods of the `AbstractNotificationProviderService` class.

Refer to the [Create Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md) guide for detailed information about the methods.

### d. Implement validateOptions Method

The `validateOptions` method is used to validate the options passed to the module provider. If the method throws an error, the Medusa application won't start.

So, add the `validateOptions` method to the `SlackNotificationProviderService` class:

```ts title="src/modules/slack/service.ts"
// other imports...
import { 
  MedusaError,
} from "@medusajs/framework/utils"

class SlackNotificationProviderService extends AbstractNotificationProviderService {
  // ...
  static validateOptions(options: Record<any, any>): void | never {
    if (!options.webhook_url) {
      throw new MedusaError(
        MedusaError.Types.INVALID_ARGUMENT,
        "Webhook URL is required"
      )
    }
    if (!options.admin_url) {
      throw new MedusaError(
        MedusaError.Types.INVALID_ARGUMENT,
        "Admin URL is required"
      )
    }
  }
}
```

The `validateOptions` method receives the options passed to the module provider as a parameter.

In the method, you throw an error if any of the options are not set.

### e. Implement send Method

When the Medusa application needs to send a notification through a channel (such as Slack), it calls the `send` method of the channel's module provider.

You'll first add helper methods that you'll use in the `send` method. Then, you'll implement the `send` method itself.

#### getDisplayAmount Method

The first method you'll add is a method to format amounts for displaying them in notifications. So, add the `getDisplayAmount` method to the `SlackNotificationProviderService` class:

```ts title="src/modules/slack/service.ts"
class SlackNotificationProviderService extends AbstractNotificationProviderService {
  // ...
  private async getDisplayAmount(amount: number, currencyCode: string) {
    return Intl.NumberFormat("en-US", {
      style: "currency",
      currency: currencyCode,
    }).format(amount)
  }
}
```

The `getDisplayAmount` method receives an amount and a currency code and returns the formatted amount as a string.

#### sendOrderNotification Method

Next, you'll add a method to format a Slack message for created orders. So, add the `sendOrderNotification` method to the `SlackNotificationProviderService` class:

```ts title="src/modules/slack/service.ts" highlights={sendOrderNotificationHighlights}
// other imports...
import { 
  OrderDTO, 
  ProviderSendNotificationDTO, 
  ProviderSendNotificationResultsDTO,
} from "@medusajs/framework/types"
import axios from "axios"

class SlackNotificationProviderService extends AbstractNotificationProviderService {
  // ...
  async sendOrderNotification(notification: ProviderSendNotificationDTO) {
    const order = notification.data?.order as OrderDTO
    if (!order) {
      throw new MedusaError(
        MedusaError.Types.NOT_FOUND,
        "Order not found"
      )
    }
    const shippingAddress = order.shipping_address
    const blocks: Record<string, unknown>[] = [
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: `Order *<${this.options.admin_url}/orders/${order.id}|#${order.display_id}>* has been created.`,
        },
      },
    ]

    if (shippingAddress) {
      blocks.push({
        type: "section",
        text: {
          type: "mrkdwn",
          text: `*Customer*\n${shippingAddress.first_name} ${
            shippingAddress.last_name
          }\n${order.email}\n*Destination*\n${
            shippingAddress.address_1
          }\n${
            shippingAddress.city
          }, ${(shippingAddress.country_code as string).toUpperCase()}`,
        },
      })
    }

    blocks.push(
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: `*Subtotal*\t${await this.getDisplayAmount(order.subtotal as number, order.currency_code)}\n*Shipping*\t${
            await this.getDisplayAmount(order.shipping_total as number, order.currency_code)
          }\n*Discount Total*\t${await this.getDisplayAmount(
            order.discount_total as number,
            order.currency_code
          )}\n*Tax*\t${await this.getDisplayAmount(order.tax_total as number, order.currency_code)}\n*Total*\t${
            await this.getDisplayAmount(order.total as number, order.currency_code)
          }`,
        },
      },
      {
        type: "divider",
      }
    )

    await Promise.all(
      order.items?.map(async (item) => {
        const line: Record<string, unknown> = {
          type: "section",
          text: {
            type: "mrkdwn",
            text: `*${item.title}*\n${item.quantity} x ${await this.getDisplayAmount(
              item.unit_price as number,
              order.currency_code
            )}`,
          },
        }
  
        if (item.thumbnail) {
          const url = item.thumbnail
  
          line.accessory = {
            type: "image",
            alt_text: "Item",
            image_url: url,
          }
        }
  
        blocks.push(line)
        blocks.push({
          type: "divider",
        })
      }) || []
    )

    await axios.post(this.options.webhook_url, {
      text: `Order ${order.display_id} was created`,
      blocks,
    })

    return {
      id: order.id,
    }
  }
}
```

The `sendOrderNotification` method receives a `ProviderSendNotificationDTO` object, which is the object passed to the `send` method that you'll implement next. The object has a `data` property that contains the order data.

In the method, you format the Slack message to show the order's ID, customer information, shipping address, order items, and total amounts. You also add a link to the order's details page in the Medusa Admin dashboard.

Finally, you send the formatted message to Slack using the `axios.post` method with the configured webhook URL.

#### send Method

You can now implement the `send` method, which is the method that Medusa calls to send notifications using the provider. Add the `send` method to the `SlackNotificationProviderService` class:

```ts title="src/modules/slack/service.ts"
class SlackNotificationProviderService extends AbstractNotificationProviderService {
  // ...
  async send(
    notification: ProviderSendNotificationDTO
  ): Promise<ProviderSendNotificationResultsDTO> {
    const { template } = notification

    switch (template) {
      case "order-created":
        return this.sendOrderNotification(notification)
      default:
        throw new MedusaError(
          MedusaError.Types.NOT_FOUND,
          `Template ${template} not supported`
        )
    }
  }
}
```

The `send` method receives a `ProviderSendNotificationDTO` object, which contains the notification data and the template to use for sending the notification.

The method receives other parameters, which you can find in the [Create Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module#send/index.html.md) guide.

In the method, you check the `template` property of the notification object. If it's `order-created`, you call the `sendOrderNotification` method to send the notification. Otherwise, you throw an error indicating that the template is not supported.

### f. Export Module Definition

You've now finished implementing the necessary methods for the Slack Notification Module Provider.

The final piece to a module is its definition, which you export in an `index.ts` file at the module's root directory. This definition tells Medusa the module's details, including its service.

To create the module's definition, create the file `src/modules/slack/index.ts` with the following content:

```ts title="src/modules/slack/index.ts"
import SlackNotificationProvider from "./service"
import { 
  ModuleProvider, 
  Modules,
} from "@medusajs/framework/utils"

export default ModuleProvider(Modules.NOTIFICATION, {
  services: [SlackNotificationProvider],
})
```

You use `ModuleProvider` from the Modules SDK to create the module provider's definition. It accepts two parameters:

1. The name of the module that this provider belongs to, which is `Modules.NOTIFICATION` in this case.
2. An object with a required property `services` indicating the Module Provider's services.

### g. Add Module Provider to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property:

```ts title="medusa-config.ts" highlights={configHighlights}
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          {
            resolve: "./src/modules/slack",
            id: "slack",
            options: {
              channels: ["slack"],
              webhook_url: process.env.SLACK_WEBHOOK_URL,
              admin_url: process.env.SLACK_ADMIN_URL,
            },
          },
        ],
      },
    },
  ],
})
```

To pass a Notification Module Provider to the Notification Module, you add the `modules` property to the Medusa configuration and pass the Notification Module in its value.

The Notification Module accepts a `providers` option, which is an array of Notification Module Providers to register.

To register the Slack Notification Module Provider, you add an object to the `providers` array with the following properties:

- `resolve`: The NPM package or path to the module provider. In this case, it's the path to the `src/modules/slack` directory.
- `id`: The ID of the module provider. The Notification Module Provider is then registered with the ID `np_{identifier}_{id}`, where:
  - `{identifier}`: The identifier static property defined in the Module Provider's service, which is `slack` in this case.
  - `{id}`: The ID set in this configuration, which is also `slack` in this case.
- `options`: The options to pass to the module provider. These are the options you defined in the `Options` interface of the module provider's service.
  - You must also set a `channel` option that indicates the channel this provider is used to send notifications.

### h. Set Options as Environment Variables

Next, you'll set the options you passed to the Slack Notification Module Provider as environment variables.

To set the webhook URL, you need to create a Slack application and configure a webhook URL. To do that:

1. Go to your [Slack Apps](https://api.slack.com/apps) page.
2. Click the "Create New App" button.

![Slack Apps page interface showing the prominent green "Create New App" button in the top navigation area, which initiates the process of creating a new Slack application for integrating with external services like Medusa](https://res.cloudinary.com/dza7lstvk/image/upload/v1748940543/Medusa%20Resources/CleanShot_2025-06-03_at_10.43.01_2x_zadmwn.png)

3. In the pop-up, choose "From scratch".
4. Enter the app's name and select the workspace to install it in.
5. Once you're done, click the "Create App" button.

![Slack app creation modal dialog with options to create app "From scratch" or "From an app manifest", including form fields for app name and workspace selection, with a "Create App" button to confirm the new Slack application setup](https://res.cloudinary.com/dza7lstvk/image/upload/v1748940602/Medusa%20Resources/CleanShot_2025-06-03_at_10.43.52_2x_edqawv.png)

6. In the app's settings, go to the "Incoming Webhooks" section.
7. Enable the "Activate Incoming Webhooks" toggle.

![Slack app configuration page showing the Incoming Webhooks settings with the "Activate Incoming Webhooks" toggle switch prominently displayed, which enables the app to receive webhook notifications from external services like Medusa](https://res.cloudinary.com/dza7lstvk/image/upload/v1748940647/Medusa%20Resources/CleanShot_2025-06-03_at_10.44.07_2x_rykabg.png)

8. In the "Webhook URLs for Your Workspace" section, click the "Add New Webhook" button.

![Webhook configuration section displaying the "Add New Webhook" button in the "Webhook URLs for Your Workspace" area, allowing users to create a new webhook endpoint for receiving automated notifications in their Slack workspace](https://res.cloudinary.com/dza7lstvk/image/upload/v1748940671/Medusa%20Resources/CleanShot_2025-06-03_at_10.44.33_2x_e7qk8o.png)

9. Select the channel or conversation to send notifications to and click the "Allow" button.
10. Copy the generated webhook URL.

![Slack webhook URL generation interface showing the newly created webhook URL with a copy button, which provides the unique endpoint URL needed to configure Medusa for sending automated notifications to the selected Slack channel](https://res.cloudinary.com/dza7lstvk/image/upload/v1748940718/Medusa%20Resources/CleanShot_2025-06-03_at_10.45.51_2x_xet5u5.png)

Then, in the `.env` file of your Medusa application, set the `SLACK_WEBHOOK_URL` and `SLACK_ADMIN_URL` environment variables:

```shell title=".env"
SLACK_WEBHOOK_URL=https://hooks.slack.com/...
SLACK_ADMIN_URL=http://localhost:9000/app
```

In development, the Medusa Admin dashboard is running on `http://localhost:9000/app`, so you can use that URL for the `SLACK_ADMIN_URL` environment variable.

You'll test out the integration when you handle the `order.placed` event in the next step.

***

## Step 3: Handle the `order.placed` Event

Now that you've integrated Slack with Medusa, you can send notifications to Slack at any point, including when an order is created.

To send a notification to Slack when an order is created, you will:

- Implement the order details retrieval and notification sending with Slack in a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of actions, called steps, that complete a task with rollback and retry mechanisms.
- Listen to the `order.placed` event in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). A subscriber is an asynchronous function that listens to events to perform actions, such as execute a workflow, when the event is emitted.

### a. Create the Workflow

The workflow to send notifications to Slack will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the order details.
- [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md): Send a notification with a configured provider.

Medusa provides both steps in its `@medusajs/medusa/core-flows` package.

So, to create the workflow, create the file `src/workflows/order-placed-notification.ts` with the following content:

```ts title="src/workflows/order-placed-notification.ts" highlights={orderPlacedNotificationWorkflowHighlights}
import { 
  createWorkflow, 
} from "@medusajs/framework/workflows-sdk"
import { sendNotificationsStep, useQueryGraphStep } from "@medusajs/medusa/core-flows"

type WorkflowInput = {
  id: string
}

export const orderPlacedNotificationWorkflow = createWorkflow(
  "order-placed-notification",
  ({ id }: WorkflowInput) => {
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "id",
        "display_id", 
        "email",
        "shipping_address.*",
        "subtotal",
        "shipping_total",
        "currency_code", 
        "discount_total",
        "tax_total",
        "total",
        "items.*",
        "original_total",
      ],
      filters: {
        id,
      },
    })

    sendNotificationsStep([{
      to: "slack-channel", // This will be configured in the Slack app
      channel: "slack",
      template: "order-created",
      data: {
        order: orders[0],
      },
    }])
  }
)
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is an object holding the ID of the order placed.

In the workflow's constructor function, you:

- Retrieve the order's details using the `useQueryGraphStep`. This step uses Medusa's [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) tool to retrieve data across modules. You pass it the order ID to retrieve.
- Send a notification using the `sendNotificationsStep`. You pass the step an array of notifications to send, and each notification is an object with the following properties:
  - `to`: The channel to send the notification to. Since you configure this in the Slack app, you just set it to `slack-channel`.
  - `channel`: The channel to use for sending the notification. By setting it to `slack`, the Notification Module will use the Slack Notification Module Provider to send the notification.
  - `template`: The template to use for sending the notification, which is `order-created` in this case.
  - `data`: The data payload to pass with the notification, which contains the order details.

You now have a workflow that retrieves the order details and sends a notification to Slack when an order is placed.

Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) documentation to learn more about workflows and steps.

### b. Create the Subscriber

To execute the workflow when an order is placed, you need to create a subscriber that listens to the `order.placed` event and executes the workflow.

To create the subscriber, create the file `src/subscribers/order-placed.ts` with the following content:

```ts title="src/subscribers/order-placed.ts"
import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
import { 
  orderPlacedNotificationWorkflow,
} from "../workflows/order-placed-notification"

export default async function orderPlacedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await orderPlacedNotificationWorkflow(container)
    .run({
      input: data,
    })
}

export const config: SubscriberConfig = {
  event: "order.placed",
}
```

A subscriber file must export:

1. An asynchronous function, which is the subscriber that is executed when the event is emitted.
2. A configuration object that holds the name of the event that the subscriber listens to, which is `order.placed` in this case.

The subscriber function receives an object as a parameter that has the following properties:

- `event`: An object that holds the event's data payload. The payload of the `order.placed` event is the ID of the order placed.
- `container`: The [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) to access the Framework and commerce tools.

In the subscriber function, you execute the `orderPlacedNotificationWorkflow` by invoking it, passing the Medusa container as a parameter. Then, you chain a `run` method, passing it the order ID from the event's data payload as input.

Refer to the [Events and Subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) documentation to learn more about creating subscribers.

### Test it Out

You'll now test out the integration by placing an order using the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-slack`, you can find the storefront by going back to the parent directory and changing to the `medusa-slack-storefront` directory:

```bash
cd ../medusa-slack-storefront # change based on your project name
```

First, start the Medusa application by running the following command in the Medusa application's directory:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

Then, start the Next.js Starter Storefront by running the following command in the storefront's directory:

```bash npm2yarn badgeLabel="Next.js Starter Storefront" badgeColor="blue"
npm run dev
```

Next, open the Next.js Starter Storefront in your browser at `http://localhost:8000`. Add a product to the cart, proceed to checkout, and place an order.

Afterwards, you'll receive a notification in the Slack channel you configured in the Slack app. The notification will contain the order details.

You can also click on the order's ID in the notification to open the order's details page in the Medusa Admin dashboard.

![Slack Notification Example](https://res.cloudinary.com/dza7lstvk/image/upload/v1748942706/Medusa%20Resources/CleanShot_2025-06-03_at_11.24.16_2x_umecps.png)

***

## Next Steps

You've now integrated Slack with Medusa to receive notifications about created orders. You can expand on this integration to send other types of notifications, such as order updates. Refer to the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for a list of events you can listen to in your subscribers.

You can also customize the notifications to include more information or change the format of the messages. Refer to [Slack's documentation](https://api.slack.com/reference/surfaces/formatting) for more information on how to format messages in Slack.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.
3. Contact the [sales team](https://medusajs.com/contact/) to get help from the Medusa team.


# Integrate Strapi (CMS) with Medusa

In this tutorial, you'll learn how to integrate [Strapi](https://strapi.io/) with Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. Medusa also facilitates integrating third-party services that enrich your application with features specific to your unique business use case.

By integrating Strapi, you can manage your products' content with powerful content management capabilities, including custom fields, media, localization, and more.

This guide was built with Strapi v5.30.1. If you're using a different version and you run into issues, consider [opening an issue](https://github.com/medusajs/medusa/issues/new?template=docs.yml).

## Summary

By following this tutorial, you'll learn how to:

- Install and set up Medusa.
- Install and set up Strapi.
- Integrate Strapi with Medusa to interact with Strapi's API.
- Implement two-way synchronization of product data between Medusa and Strapi:
  - Handle product events to sync data from Medusa to Strapi.
  - Handle Strapi webhooks to sync data from Strapi to Medusa.
- Display product data from Strapi in the Next.js Starter Storefront.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer, but you're expected to have knowledge in Strapi, as its concepts are not explained in the tutorial.

![Diagram illustrating the flow of data between Medusa, Strapi, admin, and customer (storefront)](https://res.cloudinary.com/dza7lstvk/image/upload/v1763375209/Medusa%20Resources/strapi-summary_pioikw.jpg)

[Full Code](https://github.com/medusajs/examples/tree/main/strapi-integration): Find the full code of the guide in this repository.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20 or v22 (Versions supported by Strapi)](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

First, you'll be asked for the project's name. Then, when prompted about installing the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Set Up Strapi

In this step, you'll install and set up Strapi to manage your product content.

### a. Install Strapi

In a separate directory from your Medusa application, run the following command to create a new Strapi project:

```bash
npx create-strapi@latest my-strapi-app
```

You can pick the default options during the installation process. Once the installation is complete, navigate to the newly created directory:

```bash
cd my-strapi-app
```

### b. Setup Strapi

Next, you'll start Strapi and create a new admin user.

Run the following command to start Strapi:

```bash badgeLabel="Strapi" badgeColor="orange"
npm run dev
```

This command starts Strapi in development mode and opens the admin panel setup page in your default browser.

On this page, you can create a new admin user to log in to the Strapi admin panel. You'll return to the admin panel later to manage settings and content.

### c. Define Product Content Type

In this section, you'll define a content type for products in Strapi. These products will be synced from Medusa, allowing you to manage their content using Strapi's CMS features.

You'll use `schema.json` files to define content types.

#### Product schema.json

To create the schema for the Product content type, create the file `src/api/product/content-types/product/schema.json` with the following content:

```json title="src/api/product/content-types/product/schema.json" badgeLabel="Strapi" badgeColor="orange"
{
  "kind": "collectionType",
  "collectionName": "products",
  "info": {
    "singularName": "product",
    "pluralName": "products",
    "displayName": "Product",
    "description": "Products from Medusa"
  },
  "options": {
    "draftAndPublish": false
  },
  "pluginOptions": {},
  "attributes": {
    "medusaId": {
      "type": "string",
      "required": true,
      "unique": true
    },
    "title": {
      "type": "string",
      "required": true
    },
    "subtitle": {
      "type": "string"
    },
    "description": {
      "type": "richtext"
    },
    "handle": {
      "type": "uid",
      "targetField": "title"
    },
    "images": {
      "type": "media",
      "multiple": true,
      "required": false,
      "allowedTypes": ["images"]
    },
    "thumbnail": {
      "type": "media",
      "multiple": false,
      "required": false,
      "allowedTypes": ["images"]
    },
    "locale": {
      "type": "string",
      "default": "en"
    },
    "variants": {
      "type": "relation",
      "relation": "oneToMany",
      "target": "api::product-variant.product-variant",
      "mappedBy": "product"
    },
    "options": {
      "type": "relation",
      "relation": "oneToMany",
      "target": "api::product-option.product-option",
      "mappedBy": "product"
    }
  }
}
```

You define the following fields for the Product content type:

1. `medusaId`: A unique identifier that maps to the Medusa product ID.
2. `title`: The product's title.
3. `subtitle`: A subtitle for the product.
4. `description`: A rich text field for the product's description.
5. `handle`: A unique identifier for the product used in URLs.
6. `images`: A media field to store multiple images of the product.
7. `thumbnail`: A media field to store a single thumbnail image of the product.
8. `locale`: A string field to support localization.
9. `variants`: A one-to-many relation to the Product Variant content type, which you'll define later.
10. `options`: A one-to-many relation to the Product Option content type, which you'll define later.

#### Product Lifecycle Hooks

Next, you'll handle product deletion by deleting associated product variants and options.

Create the file `src/api/product/content-types/product/lifecycles.ts` with the following content:

```ts title="src/api/product/content-types/product/lifecycles.ts" badgeLabel="Strapi" badgeColor="orange"
export default {
  async beforeDelete(event) {
    const { where } = event.params
    
    // Find the product with its relations
    const product = await strapi.db.query("api::product.product").findOne({
      where: {
        id: where.id,
      },
      populate: {
        variants: true,
        options: true,
      },
    })

    if (product) {
      // Delete all variants
      if (product.variants && product.variants.length > 0) {
        for (const variant of product.variants) {
          await strapi.documents("api::product-variant.product-variant").delete({
            documentId: variant.documentId,
          })
        }
      }

      // Delete all options (their values will
      // be cascade deleted by the option lifecycle)
      if (product.options && product.options.length > 0) {
        for (const option of product.options) {
          await strapi.documents("api::product-option.product-option").delete({
            documentId: option.documentId,
          })
        }
      }
    }
  },
}
```

You define a `beforeDelete` lifecycle hook that deletes all associated product variants and options when a product is deleted.

#### Product Controllers

Next, you'll create custom controllers to handle product management.

Create the file `src/api/product/controllers/product.ts` with the following content:

```ts title="src/api/product/controllers/product.ts" badgeLabel="Strapi" badgeColor="orange"
import { factories } from "@strapi/strapi"

export default factories.createCoreController("api::product.product")
```

This code creates a core controller for the Product content type using Strapi's factory method.

#### Product Services

Next, you'll create custom services to handle product management.

Create the file `src/api/product/services/product.ts` with the following content:

```ts title="src/api/product/services/product.ts" badgeLabel="Strapi" badgeColor="orange"
import { factories } from "@strapi/strapi"

export default factories.createCoreService("api::product.product")
```

This code creates a core service for the Product content type using Strapi's factory method.

#### Product Routes

Next, you'll create custom routes to handle product management.

Create the file `src/api/product/routes/product.ts` with the following content:

```ts title="src/api/product/routes/product.ts" badgeLabel="Strapi" badgeColor="orange"
import { factories } from "@strapi/strapi"

export default factories.createCoreRouter("api::product.product")
```

This code creates a core router for the Product content type using Strapi's factory method.

### c. Define Product Variant Content Type

Next, you'll define a content type for product variants in Strapi.

#### Product Variant schema.json

To create the schema for the Product Variant content type, create the file `src/api/product-variant/content-types/product-variant/schema.json` with the following content:

```json title="src/api/product-variant/content-types/product-variant/schema.json" badgeLabel="Strapi" badgeColor="orange"
{
  "kind": "collectionType",
  "collectionName": "product_variants",
  "info": {
    "singularName": "product-variant",
    "pluralName": "product-variants",
    "displayName": "Product Variant",
    "description": "Product variants from Medusa"
  },
  "options": {
    "draftAndPublish": false
  },
  "pluginOptions": {},
  "attributes": {
    "medusaId": {
      "type": "string",
      "required": true,
      "unique": true
    },
    "title": {
      "type": "string",
      "required": true
    },
    "sku": {
      "type": "string"
    },
    "images": {
      "type": "media",
      "multiple": true,
      "required": false,
      "allowedTypes": ["images"]
    },
    "thumbnail": {
      "type": "media",
      "multiple": false,
      "required": false,
      "allowedTypes": ["images"]
    },
    "locale": {
      "type": "string",
      "default": "en"
    },
    "product": {
      "type": "relation",
      "relation": "manyToOne",
      "target": "api::product.product",
      "inversedBy": "variants"
    },
    "option_values": {
      "type": "relation",
      "relation": "manyToMany",
      "target": "api::product-option-value.product-option-value",
      "inversedBy": "variants"
    }
  }
}
```

You define the following fields for the Product Variant content type:

1. `medusaId`: A unique identifier that maps to the Medusa product variant ID.
2. `title`: The variant's title.
3. `sku`: The stock keeping unit for the variant.
4. `images`: A media field to store multiple images of the variant.
5. `thumbnail`: A media field to store a single thumbnail image of the variant.
6. `locale`: A string field to support localization.
7. `product`: A many-to-one relation to the Product content type.
8. `option_values`: A many-to-many relation to the Product Option Value content type, which you'll define later.

#### Product Variant Controllers

Next, you'll create custom controllers to handle product variant management.

Create the file `src/api/product-variant/controllers/product-variant.ts` with the following content:

```ts title="src/api/product-variant/controllers/product-variant.ts" badgeLabel="Strapi" badgeColor="orange"
import { factories } from "@strapi/strapi"

export default factories.createCoreController("api::product-variant.product-variant")
```

This code creates a core controller for the Product Variant content type using Strapi's factory method.

#### Product Variant Services

Next, you'll create custom services to handle product variant management.

Create the file `src/api/product-variant/services/product-variant.ts` with the following content:

```ts title="src/api/product-variant/services/product-variant.ts" badgeLabel="Strapi" badgeColor="orange"
import { factories } from "@strapi/strapi"

export default factories.createCoreService("api::product-variant.product-variant")
```

This code creates a core service for the Product Variant content type using Strapi's factory method.

#### Product Variant Routes

Next, you'll create custom routes to handle product variant management.

Create the file `src/api/product-variant/routes/product-variant.ts` with the following content:

```ts title="src/api/product-variant/routes/product-variant.ts" badgeLabel="Strapi" badgeColor="orange"
import { factories } from "@strapi/strapi"

export default factories.createCoreRouter("api::product-variant.product-variant")
```

This code creates a core router for the Product Variant content type using Strapi's factory method.

### d. Define Product Option Content Type

Next, you'll define a content type for product options in Strapi.

#### Product Option schema.json

To create the schema for the Product Option content type, create the file `src/api/product-option/content-types/product-option/schema.json` with the following content:

```json title="src/api/product-option/content-types/product-option/schema.json" badgeLabel="Strapi" badgeColor="orange"
{
  "kind": "collectionType",
  "collectionName": "product_options",
  "info": {
    "singularName": "product-option",
    "pluralName": "product-options",
    "displayName": "Product Option",
    "description": "Product options from Medusa"
  },
  "options": {
    "draftAndPublish": false
  },
  "pluginOptions": {},
  "attributes": {
    "medusaId": {
      "type": "string",
      "required": true,
      "unique": true
    },
    "title": {
      "type": "string",
      "required": true
    },
    "locale": {
      "type": "string",
      "default": "en"
    },
    "product": {
      "type": "relation",
      "relation": "manyToOne",
      "target": "api::product.product",
      "inversedBy": "options"
    },
    "values": {
      "type": "relation",
      "relation": "oneToMany",
      "target": "api::product-option-value.product-option-value",
      "mappedBy": "option"
    }
  }
}
```

You define the following fields for the Product Option content type:

1. `medusaId`: A unique identifier that maps to the Medusa product option ID.
2. `title`: The option's title.
3. `locale`: A string field to support localization.
4. `product`: A many-to-one relation to the Product content type.
5. `values`: A one-to-many relation to the Product Option Value content type, which you'll define later.

#### Product Option Lifecycle Hooks

Next, you'll handle option deletion by deleting associated option values.

Create the file `src/api/product-option/content-types/product-option/lifecycles.ts` with the following content:

```ts title="src/api/product-option/content-types/product-option/lifecycles.ts" badgeLabel="Strapi" badgeColor="orange"
export default {
  async beforeDelete(event) {
    const { where } = event.params
    
    // Find the option with its values
    const option = await strapi.db.query("api::product-option.product-option").findOne({
      where: {
        id: where.id,
      },
      populate: {
        values: true,
      },
    })

    if (option && option.values && option.values.length > 0) {
      // Delete all option values
      for (const value of option.values) {
        await strapi.documents("api::product-option-value.product-option-value").delete({
          documentId: value.documentId,
        })
      }
    }
  },
}
```

You define a `beforeDelete` lifecycle hook that deletes all associated option values when an option is deleted.

#### Product Option Controllers

Next, you'll create custom controllers to handle managing product options.

Create the file `src/api/product-option/controllers/product-option.ts` with the following content:

```ts title="src/api/product-option/controllers/product-option.ts" badgeLabel="Strapi" badgeColor="orange"
import { factories } from "@strapi/strapi"

export default factories.createCoreController("api::product-option.product-option")
```

This code creates a core controller for the Product Option content type using Strapi's factory method.

#### Product Option Services

Next, you'll create custom services to handle managing product options.

Create the file `src/api/product-option/services/product-option.ts` with the following content:

```ts title="src/api/product-option/services/product-option.ts" badgeLabel="Strapi" badgeColor="orange"
import { factories } from "@strapi/strapi"

export default factories.createCoreService("api::product-option.product-option")
```

This code creates a core service for the Product Option content type using Strapi's factory method.

#### Product Option Routes

Next, you'll create custom routes to handle managing product options.

Create the file `src/api/product-option/routes/product-option.ts` with the following content:

```ts title="src/api/product-option/routes/product-option.ts" badgeLabel="Strapi" badgeColor="orange"
import { factories } from "@strapi/strapi"

export default factories.createCoreRouter("api::product-option.product-option")
```

This code creates a core router for the Product Option content type using Strapi's factory method.

### e. Define Product Option Value Content Type

The last content type you'll define is for product option values in Strapi.

#### Product Option Value schema.json

To create the schema for the Product Option Value content type, create the file `src/api/product-option-value/content-types/product-option-value/schema.json` with the following content:

```json title="src/api/product-option-value/content-types/product-option-value/schema.json" badgeLabel="Strapi" badgeColor="orange"
{
  "kind": "collectionType",
  "collectionName": "product_option_values",
  "info": {
    "singularName": "product-option-value",
    "pluralName": "product-option-values",
    "displayName": "Product Option Value",
    "description": "Product option values from Medusa"
  },
  "options": {
    "draftAndPublish": false
  },
  "pluginOptions": {},
  "attributes": {
    "medusaId": {
      "type": "string",
      "required": true,
      "unique": true
    },
    "value": {
      "type": "string",
      "required": true
    },
    "locale": {
      "type": "string",
      "default": "en"
    },
    "option": {
      "type": "relation",
      "relation": "manyToOne",
      "target": "api::product-option.product-option",
      "inversedBy": "values"
    },
    "variants": {
      "type": "relation",
      "relation": "manyToMany",
      "target": "api::product-variant.product-variant",
      "mappedBy": "option_values"
    }
  }
}
```

You define the following fields for the Product Option Value content type:

1. `medusaId`: A unique identifier that maps to the Medusa product option value ID.
2. `value`: The option value's title.
3. `locale`: A string field to support localization.
4. `option`: A many-to-one relation to the Product Option content type.
5. `variants`: A many-to-many relation to the Product Variant content type.

#### Product Option Value Controllers

Next, you'll create custom controllers to handle managing product option values.

Create the file `src/api/product-option-value/controllers/product-option-value.ts` with the following content:

```ts title="src/api/product-option-value/controllers/product-option-value.ts" badgeLabel="Strapi" badgeColor="orange"
import { factories } from "@strapi/strapi"

export default factories.createCoreController("api::product-option-value.product-option-value")
```

This code creates a core controller for the Product Option Value content type using Strapi's factory method.

#### Product Option Value Services

Next, you'll create custom services to handle managing product option values.

Create the file `src/api/product-option-value/services/product-option-value.ts` with the following content:

```ts title="src/api/product-option-value/services/product-option-value.ts" badgeLabel="Strapi" badgeColor="orange"
import { factories } from "@strapi/strapi"

export default factories.createCoreService("api::product-option-value.product-option-value")
```

This code creates a core service for the Product Option Value content type using Strapi's factory method.

#### Product Option Value Routes

Next, you'll create custom routes to handle managing product option values.

Create the file `src/api/product-option-value/routes/product-option-value.ts` with the following content:

```ts title="src/api/product-option-value/routes/product-option-value.ts" badgeLabel="Strapi" badgeColor="orange"
import { factories } from "@strapi/strapi"

export default factories.createCoreRouter("api::product-option-value.product-option-value")
```

This code creates a core router for the Product Option Value content type using Strapi's factory method.

You now have all the customizations in Strapi ready. You'll return to Strapi later after you set up the integration with Medusa.

***

## Step 3: Integrate Strapi with Medusa

In this step, you'll integrate Strapi with Medusa by creating a Strapi Module.

A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a reusable package that provides functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules and their structure.

### a. Install Strapi Client

First, you'll install the Strapi client in your Medusa application to interact with Strapi's API.

In your Medusa application directory, run the following command to install the Strapi client:

```bash badgeLabel="Medusa application" badgeColor="green"
npm install @strapi/client
```

### b. Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/strapi`.

### c. Create Strapi Client Loader

Next, you'll create the Strapi client when the Medusa server starts by creating a loader.

A [loader](https://docs.medusajs.com/docs/learn/fundamentals/modules/loaders/index.html.md) is an asynchronous function that runs when the Medusa server starts. Loaders are useful for setting up connections to third-party services and reusing those connections throughout your module.

To create the loader that initializes the Strapi client, create the file `src/modules/strapi/loaders/init-client.ts` with the following content:

```ts title="src/modules/strapi/loaders/init-client.ts" badgeLabel="Medusa application" badgeColor="green"
import { LoaderOptions } from "@medusajs/framework/types"
import { asValue } from "@medusajs/framework/awilix"
import { MedusaError } from "@medusajs/framework/utils"
import { strapi } from "@strapi/client"

export type ModuleOptions = {
  apiUrl: string
  apiToken: string
  defaultLocale?: string
}

export default async function initStrapiClientLoader({
  container,
  options,
}: LoaderOptions<ModuleOptions>) {
  if (!options?.apiUrl || !options?.apiToken) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "Strapi API URL and token are required"
    )
  }

  const logger = container.resolve("logger")

  try {
    // Create Strapi client instance
    const strapiClient = strapi({
      baseURL: options.apiUrl,
      auth: options.apiToken,
    })

    // Register the client in the container
    container.register({
      strapiClient: asValue(strapiClient),
    })

    logger.info("Strapi client initialized successfully")
  } catch (error) {
    logger.error(`Failed to initialize Strapi client: ${error}`)
    throw error
  }
}
```

A loader file must export an asynchronous function that receives an object with the following properties:

1. `container`: The [module container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) that allows you to resolve and register module and Framework resources.
2. `options`: The options passed to the module during its registration. You define the following options for the Strapi Module:
   - `apiUrl`: The URL of the Strapi API.
   - `apiToken`: The API token to authenticate requests to Strapi.
   - `defaultLocale`: An optional default locale for content.

In the loader function, you create a Strapi client instance using the provided API URL and token. Then, you register the client in the module container so that it can be resolved and used in the module's service.

### d. Create Strapi Module Service

Next, you'll create the main service of the Strapi Module.

A module has a service that contains its logic. The Strapi Module's service will contain the logic to create, update, retrieve, and delete data in Strapi.

Create the file `src/modules/strapi/service.ts` with the following content:

```ts title="src/modules/strapi/service.ts" badgeLabel="Medusa application" badgeColor="green"
import type { StrapiClient } from "@strapi/client"
import { Logger } from "@medusajs/framework/types"
import { ModuleOptions } from "./loaders/init-client"

type InjectedDependencies = {
  logger: Logger
  strapiClient: StrapiClient
}

export default class StrapiModuleService {
  protected readonly options_: ModuleOptions
  protected readonly logger_: any
  protected readonly client_: StrapiClient

  constructor(
    { logger, strapiClient }: InjectedDependencies, 
    options: ModuleOptions
  ) {
    this.options_ = options
    this.logger_ = logger
    this.client_ = strapiClient
  }

  // TODO add methods
}
```

The constructor of a module's service receives the following parameters:

1. The module's container.
2. The module's options.

You resolve the [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) and the Strapi client that you registered in the loader. You also store the module options for later use.

In the next sections, you'll add methods to this service to handle managing data in Strapi.

#### Format Errors Method

First, you'll add a helper method to format errors from Strapi.

In `src/modules/strapi/service.ts`, add the following method to the `StrapiModuleService` class:

```ts title="src/modules/strapi/service.ts" badgeLabel="Medusa application" badgeColor="green"
export default class StrapiModuleService {
  // ...
  formatStrapiError(error: any, context: string): string {
    // Handle Strapi client HTTP response errors
    if (error?.response) {
      const response = error.response
      const parts = [context]
      
      if (response.status) {
        parts.push(`HTTP ${response.status}`)
      }
      
      if (response.statusText) {
        parts.push(response.statusText)
      }
      
      // Add request URL if available
      if (response.url) {
        parts.push(`URL: ${response.url}`)
      }
      
      // Add request method if available
      if (error.request?.method) {
        parts.push(`Method: ${error.request.method}`)
      }
      
      return parts.join(" - ")
    }
    
    // If error has a response with Strapi error structure
    if (error?.error) {
      const strapiError = error.error
      const parts = [context]
      
      if (strapiError.status) {
        parts.push(`Status ${strapiError.status}`)
      }
      
      if (strapiError.name) {
        parts.push(`[${strapiError.name}]`)
      }
      
      if (strapiError.message) {
        parts.push(strapiError.message)
      }
      
      if (strapiError.details && Object.keys(strapiError.details).length > 0) {
        parts.push(`Details: ${JSON.stringify(strapiError.details)}`)
      }
      
      return parts.join(" - ")
    }
    
    // Fallback for non-Strapi errors
    return `${context}: ${error.message || error}`
  }
}
```

This method takes an error object and a context string as parameters. It formats the error based on the structure of Strapi client errors, making it easier to log and debug issues related to Strapi API requests.

You'll use this method in other service methods to handle errors consistently.

#### Upload Images Method

Next, you'll add a method to upload images to Strapi.

In `src/modules/strapi/service.ts`, add the following method to the `StrapiModuleService` class:

```ts title="src/modules/strapi/service.ts" badgeLabel="Medusa application" badgeColor="green"
export default class StrapiModuleService {
  // ...
  async uploadImages(imageUrls: string[]): Promise<number[]> {
    const uploadedIds: number[] = []

    for (const imageUrl of imageUrls) {
      try {
        // Fetch the image from the URL
        const imageResponse = await fetch(imageUrl)
        if (!imageResponse.ok) {
          this.logger_.warn(`Failed to fetch image: ${imageUrl}`)
          continue
        }

        const imageBuffer = await imageResponse.arrayBuffer()
        
        // Extract filename from URL or generate one
        const urlParts = imageUrl.split("/")
        const filename = urlParts[urlParts.length - 1] || `image-${Date.now()}.jpg`

        // Create a Blob from the buffer
        const blob = new Blob([imageBuffer], {
          type: imageResponse.headers.get("content-type") || "image/jpeg",
        })

        // Upload to Strapi using the files API
        const result = await this.client_.files.upload(blob, {
          fileInfo: {
            name: filename,
          },
        })
        
        if (result && result[0] && result[0].id) {
          uploadedIds.push(result[0].id)
        }
      } catch (error) {
        this.logger_.error(this.formatStrapiError(error, `Failed to upload image ${imageUrl}`))
      }
    }

    return uploadedIds
  }
}
```

This method takes an array of image URLs, fetches each image, and uploads it to Strapi using the Strapi client's files API. It returns an array of uploaded image IDs.

You'll use this method later when creating or updating products and product variants in Strapi.

#### Delete Images Method

Next, you'll add a method to delete images from Strapi. This will be useful when reverting changes if a failure occurs.

In `src/modules/strapi/service.ts`, add the following import at the top of the file:

```ts title="src/modules/strapi/service.ts" badgeLabel="Medusa application" badgeColor="green"
import { MedusaError } from "@medusajs/framework/utils"
```

Then, add the following method to the `StrapiModuleService` class:

```ts title="src/modules/strapi/service.ts" badgeLabel="Medusa application" badgeColor="green"
export default class StrapiModuleService {
  // ...
  async deleteImage(imageId: number): Promise<void> {
    try {
      await this.client_.files.delete(imageId)
    } catch (error) {
      throw new MedusaError(
        MedusaError.Types.UNEXPECTED_STATE,
        this.formatStrapiError(error, `Failed to delete image ${imageId} from Strapi`)
      )
    }
  }
}
```

This method takes an image ID as a parameter and deletes the corresponding image from Strapi using the Strapi client's files API. If the deletion fails, it throws a `MedusaError` with a formatted error message.

#### Create Document Type Method

Next, you'll add a method to create a document of a content type in Strapi, such as a product or product variant.

In `src/modules/strapi/service.ts`, add the following enum type before the `StrapiModuleService` class:

```ts title="src/modules/strapi/service.ts" badgeLabel="Medusa application" badgeColor="green"
export enum Collection {
  PRODUCTS = "products",
  PRODUCT_VARIANTS = "product-variants",
  PRODUCT_OPTIONS = "product-options",
  PRODUCT_OPTION_VALUES = "product-option-values",
}
```

Then, add the following method to the `StrapiModuleService` class:

```ts title="src/modules/strapi/service.ts" badgeLabel="Medusa application" badgeColor="green"
export default class StrapiModuleService {
  // ...
  async create(collection: Collection, data: Record<string, unknown>) {
    try {
      return await this.client_.collection(collection).create(data)
    } catch (error) {
      throw new MedusaError(
        MedusaError.Types.UNEXPECTED_STATE,
        this.formatStrapiError(error, `Failed to create ${collection} in Strapi`)
      )
    }
  }
}
```

This method takes the following parameters:

1. `collection`: The collection (content type) in which to create the document. It uses the `Collection` enum.
2. `data`: An object containing the data for the document to be created.

In the method, you create the document and return it.

#### Update Document Method

Next, you'll add a method to update a document of a content type in Strapi. This will be useful to implement two-way synching between Medusa and Strapi.

In `src/modules/strapi/service.ts`, add the following method to the `StrapiModuleService` class:

```ts title="src/modules/strapi/service.ts" badgeLabel="Medusa application" badgeColor="green"
export default class StrapiModuleService {
  // ...
  async update(collection: Collection, id: string, data: Record<string, unknown>) {
    try {
      return await this.client_.collection(collection).update(id, data)
    } catch (error) {
      throw new MedusaError(
        MedusaError.Types.UNEXPECTED_STATE,
        this.formatStrapiError(error, `Failed to update ${collection} in Strapi`)
      )
    }
  }
}
```

This method takes the following parameters:

1. `collection`: The collection (content type) in which the document exists. It uses the `Collection` enum.
2. `id`: The ID of the document to be updated.
3. `data`: An object containing the data to update the document with.

In the method, you update the document and return it.

#### Delete Document Method

Next, you'll add a method to delete a document of a content type from Strapi. You'll use this method when a document is deleted in Medusa, or when reverting document creation in case of failures.

In `src/modules/strapi/service.ts`, add the following method to the `StrapiModuleService` class:

```ts title="src/modules/strapi/service.ts" badgeLabel="Medusa application" badgeColor="green"
export default class StrapiModuleService {
  // ...
  async delete(collection: Collection, id: string) {
    try {
      return await this.client_.collection(collection).delete(id)
    } catch (error) {
      throw new MedusaError(
        MedusaError.Types.UNEXPECTED_STATE,
        this.formatStrapiError(error, `Failed to delete ${collection} in Strapi`)
      )
    }
  }
}
```

This method takes the following parameters:

1. `collection`: The collection (content type) in which the document exists. It uses the `Collection` enum.
2. `id`: The ID of the document to be deleted.

In the method, you delete the document.

#### Retrieve Document by Medusa ID Method

Next, you'll add a method to retrieve a document of a content type from Strapi by its Medusa ID. This will be useful to retrieve a document in case you need to revert changes.

In `src/modules/strapi/service.ts`, add the following method to the `StrapiModuleService` class:

```ts title="src/modules/strapi/service.ts" badgeLabel="Medusa application" badgeColor="green"
export default class StrapiModuleService {
  // ...
  async findByMedusaId(
    collection: Collection, 
    medusaId: string, 
    populate?: string[]
  ) {
    try {
      const result = await this.client_.collection(collection).find({
        filters: {
          medusaId: {
            $eq: medusaId,
          },
        },
        populate,
      })

      return result.data[0]
    }
    catch (error) {
      throw new MedusaError(
        MedusaError.Types.UNEXPECTED_STATE,
        this.formatStrapiError(error, `Failed to find ${collection} in Strapi`)
      )
    }
  }
}
```

This method takes the following parameters:

1. `collection`: The collection (content type) in which the document exists. It uses the `Collection` enum.
2. `medusaId`: The Medusa ID of the document to be retrieved.
3. `populate`: An optional array of relations to populate in the retrieved document.

In the method, you retrieve the documents and return the first result.

### e. Export Module Definition

The final piece of a module is its definition, which you export in an `index.ts` file at the module's root directory. This definition tells Medusa the name of the module, its service, and optionally its loaders.

To create the module's definition, create the file `src/modules/strapi/index.ts` with the following content:

```ts title="src/modules/strapi/index.ts" badgeLabel="Medusa application" badgeColor="green"
import { Module } from "@medusajs/framework/utils"
import StrapiModuleService from "./service"
import initStrapiClientLoader from "./loaders/init-client"

export const STRAPI_MODULE = "strapi"

export default Module(STRAPI_MODULE, {
  service: StrapiModuleService,
  loaders: [initStrapiClientLoader],
})
```

You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `strapi`.
2. An object with a required `service` property indicating the module's service. You also pass the loader you created to ensure it's executed when the application starts.

Aside from the module definition, you export the module's name as `STRAPI_MODULE` for later reference.

### f. Add Module to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:

```ts title="medusa-config.ts" badgeLabel="Medusa application" badgeColor="green"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./modules/strapi",
      options: {
        apiUrl: process.env.STRAPI_API_URL || "http://localhost:1337/api",
        apiToken: process.env.STRAPI_API_TOKEN || "",
        defaultLocale: process.env.STRAPI_DEFAULT_LOCALE || "en",
      },
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory or an npm package's name.

You also pass an `options` property with the module's options. You'll set the values of these options next.

### g. Set Environment Variables

Before you can use the Strapi Module, you need to set the environment variables it requires.

One of these options is an API token that has permissions to manage the content types you created in Strapi.

To retrieve the API token from Strapi, run the following command in the Strapi project directory to start the Strapi server:

```bash npm2yarn
npm run dev
```

Then:

1. Log in to the Strapi admin panel at `http://localhost:1337/admin`.
2. Go to Settings -> API Tokens.
3. Click on "Create new API Token".

![Strapi dashboard with the API Token settings opened](https://res.cloudinary.com/dza7lstvk/image/upload/v1763136134/Medusa%20Resources/CleanShot_2025-11-14_at_18.00.44_2x_ec8lds.png)

4. In the API token form:
   - Set a name for the token, such as "Medusa".
   - Set the type to "Custom", and:
     - For each content type you created (products, product variants, product options, and product option values), expand its permissions section and enable all the permissions (create, read, update, delete, find).
     - Also enable the permissions for "Upload" to allow image uploads.
5. Click on "Save".

![Strapi dashboard with the Create API Token form filled](https://res.cloudinary.com/dza7lstvk/image/upload/v1763136134/Medusa%20Resources/CleanShot_2025-11-14_at_18.01.25_2x_hdex6b.png)

Then, copy the generated API token.

Finally, set the following environment variables in your Medusa project's `.env` file:

```env
STRAPI_API_URL=http://localhost:1337/api
STRAPI_API_TOKEN=your_generated_api_token
```

Make sure to replace `your_generated_api_token` with the actual API token you copied from Strapi.

***

## Step 4: Create Virtual Read-Only Link to Strapi Products

Medusa's [Module Links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md) feature allows you to virtually link data models from external services to modules in your Medusa application. Then, when you retrieve data from Medusa, you can also retrieve the linked data from the third-party service automatically.

In this step, you'll define a virtual read-only link between the product content type in Strapi and the `Product` model in Medusa. Later, you'll be able to retrieve products from Strapi when retrieving products in Medusa.

### a. Define the Link

To define a virtual read-only link, create the file `src/links/product-strapi.ts` with the following content:

```ts title="src/links/product-strapi.ts" badgeLabel="Medusa application" badgeColor="green"
import { defineLink } from "@medusajs/framework/utils"
import ProductModule from "@medusajs/medusa/product"
import { STRAPI_MODULE } from "../modules/strapi"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    field: "id",
  },
  {
    linkable: {
      serviceName: STRAPI_MODULE,
      alias: "strapi_product",
      primaryKey: "product_id",
    },
  },
  {
    readOnly: true,
  }
)
```

The `defineLink` function accepts three parameters:

- An object of the first data model that is part of the link. In this case, it's the `Product` model from Medusa's Product Module.
- An object of the second data model that is part of the link. In this case, it's the product content type from the Strapi Module. You set the following properties:
  - `serviceName`: the name of the Strapi Module, which is `strapi`.
  - `alias`: an alias for the linked data model, which is `strapi_product`. You'll use this alias to reference the linked data model in queries.
  - `primaryKey`: the primary key of the linked data model, which is `product_id`. Medusa will look for this field in the retrieved `Products` from Strapi to match it with the `id` field of the `Product` model.
- An object with the `readOnly` property set to `true`, indicating that this link is read-only. This means you can only retrieve the linked data, but you don't manage the link in the database.

### b. Add list Method to the Strapi Module Service

When you retrieve products from Medusa with their `strapi_product` link, Medusa will call the `list` method of the Strapi Module's service to retrieve the linked products from Strapi.

So, in `src/modules/strapi/service.ts`, add a `list` method to the `StrapiModuleService` class:

```ts title="src/modules/strapi/service.ts" badgeLabel="Medusa application" badgeColor="green"
export default class StrapiModuleService {
  // ...
  async list(filter: { product_id: string | string[] }) {
    const ids = Array.isArray(filter.product_id) 
      ? filter.product_id 
      : [filter.product_id]
    
    const results: any[] = []
    
    for (const productId of ids) {
      try {
        // Fetch product with all relations populated
        const result = await this.client_.collection("products").find({
          filters: {
            medusaId: {
              $eq: productId,
            },
          },
          populate: {
            variants: {
              populate: ["option_values"],
            },
            options: {
              populate: ["values"],
            },
          },
        })
        
        if (result.data && result.data.length > 0) {
          const product = result.data[0]
          results.push({
            ...product,
            id: `${product.id}`,
            product_id: productId,
            // Include populated relations
            variants: (product.variants || []).map((variant) => ({
              ...variant,
              id: `${variant.id}`,
              option_values: (variant.option_values || []).map((option_value) => ({
                ...option_value,
                id: `${option_value.id}`,
              })),
            })),
            options: (product.options || []).map((option) => ({
              ...option,
              id: `${option.id}`,
              values: (option.values || []).map((value) => ({
                ...value,
                id: `${value.id}`,
              })),
            })),
          })
        }
      } catch (error) {
        this.logger_.warn(this.formatStrapiError(error, `Failed to fetch product ${productId} from Strapi`))
      }
    }
    
    return results
  }
}
```

The `list` method receives a `filter` object with a `product_id` property, which contains the Medusa product ID(s) to retrieve their corresponding data from Strapi.

In the method, you fetch each product from Strapi using the Strapi client's collection API, populating its relations (variants and options). You then format the retrieved data to match the expected structure and return an array of products.

You can now retrieve product data from Strapi when retrieving products in Medusa. You'll learn how to do this in the upcoming steps.

***

## Step 5: Handle Product Creation

In this step, you'll implement the logic to listen to product creation events in Medusa and create the corresponding product data in Strapi.

To do this, you'll create:

1. [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) that implement the logic to create product data in Strapi.
2. A [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that listens to the product creation event in Medusa and triggers the workflow.

### a. Create Product Options Workflow

Before creating the main workflow to handle product creation, you'll create a sub-workflow to handle the creation of product options and their values in Strapi. You'll use this sub-workflow in the main product creation workflow.

You create custom commerce features in [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task. A workflow is similar to a function, but allows you to track execution progress, define rollback logic, and configure other advanced features.

Refer to the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) to learn more.

The workflow to create product options in Strapi has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve product option data.
- [createOptionsInStrapiStep](#createOptionsInStrapiStep): Create the product option in Strapi.
- [createOptionValuesInStrapiStep](#createOptionValuesInStrapiStep): Create the product option value in Strapi.
- [updateProductOptionValuesMetadataStep](#updateProductOptionValuesMetadataStep): Store the Strapi ID in the product option values metadata.

The first step is available out-of-the-box in Medusa. You need to create the rest of the steps.

#### createOptionsInStrapiStep

The `createOptionsInStrapiStep` creates product options in Strapi.

To create the step, create the file `src/workflows/steps/create-options-in-strapi.ts` with the following content:

```ts title="src/workflows/steps/create-options-in-strapi.ts" badgeLabel="Medusa application" badgeColor="green"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { STRAPI_MODULE } from "../../modules/strapi"
import StrapiModuleService, { Collection } from "../../modules/strapi/service"

export type CreateOptionsInStrapiInput = {
  options: {
    id: string
    title: string
    strapiProductId: number
  }[]
}

export const createOptionsInStrapiStep = createStep(
  "create-options-in-strapi",
  async ({ options }: CreateOptionsInStrapiInput, { container }) => {
    const strapiService: StrapiModuleService = container.resolve(STRAPI_MODULE)

    const results: Record<string, any>[] = []

    try {
      for (const option of options) {
        // Create option in Strapi
        const strapiOption = await strapiService.create(
          Collection.PRODUCT_OPTIONS, 
          {
            medusaId: option.id,
            title: option.title,
            product: option.strapiProductId,
          }
        )

        results.push(strapiOption.data)
      }
    } catch (error) {
      // If error occurs during loop,
      // pass results created so far to compensation
      return StepResponse.permanentFailure(
        strapiService.formatStrapiError(
          error, 
          "Failed to create options in Strapi"
        ),
        { results }
      )
    }

    return new StepResponse(
      results,
      results
    )
  },
  async (compensationData, { container }) => {
    if (!compensationData) {
      return
    }

    const strapiService: StrapiModuleService = container.resolve(STRAPI_MODULE)

    // Delete all created options
    for (const result of compensationData) {
      await strapiService.delete(Collection.PRODUCT_OPTIONS, result.documentId)
    }
  }
)
```

You create a step with the `createStep` function. It accepts three parameters:

1. The step's unique name.
2. An async function that receives two parameters:
   - The step's input, which is an object holding the product options to create in Strapi.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.
3. An async compensation function that undoes the actions performed by the step function. This function is only executed if an error occurs during the workflow's execution.

In the step function, you resolve the Strapi Module's service from the Medusa container. Then, you loop through the product options and create them in Strapi using the service's `create` method.

If an error occurs during the creation loop, you return a permanent failure response with the results created so far. This allows the compensation function to delete any options that were successfully created before the error occurred.

Finally, a step must return a `StepResponse` instance, which accepts two parameters:

1. The step's output, which is an array of created Strapi product options.
2. The data to pass to the compensation function, which is also the array of created Strapi product options.

In the compensation function, you delete all the created product options in Strapi if an error occurs during the workflow's execution.

#### createOptionValuesInStrapiStep

The `createOptionValuesInStrapiStep` creates product option values in Strapi.

To create the step, create the file `src/workflows/steps/create-option-values-in-strapi.ts` with the following content:

```ts title="src/workflows/steps/create-option-values-in-strapi.ts" badgeLabel="Medusa application" badgeColor="green"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { STRAPI_MODULE } from "../../modules/strapi"
import StrapiModuleService, { Collection } from "../../modules/strapi/service"

export type CreateOptionValuesInStrapiInput = {
  optionValues: {
    id: string
    value: string
    strapiOptionId: number
  }[]
}

export const createOptionValuesInStrapiStep = createStep(
  "create-option-values-in-strapi",
  async ({ optionValues }: CreateOptionValuesInStrapiInput, { container }) => {
    const strapiService: StrapiModuleService = container.resolve(STRAPI_MODULE)

    const results: Record<string, any>[] = []

    try {
      for (const optionValue of optionValues) {
        // Create option value in Strapi
        const strapiOptionValue = await strapiService.create(
          Collection.PRODUCT_OPTION_VALUES,
          {
            medusaId: optionValue.id,
            value: optionValue.value,
            option: optionValue.strapiOptionId,
          }
        )

        results.push(strapiOptionValue.data)
      }
    } catch (error) {
      // If error occurs during loop,
      // pass results created so far to compensation
      return StepResponse.permanentFailure(
        strapiService.formatStrapiError(
          error, 
          "Failed to create option values in Strapi"
        ),
        { results }
      )
    }

    return new StepResponse(
      results,
      results
    )
  },
  async (compensationData, { container }) => {
    if (!compensationData) {
      return
    }

    const strapiService: StrapiModuleService = container.resolve(STRAPI_MODULE)
    
    // Delete all created option values
    for (const result of compensationData) {
      await strapiService.delete(
        Collection.PRODUCT_OPTION_VALUES, 
        result.documentId
      )
    }
  }
)
```

This step receives the option values to create in Strapi. In the step, you create each option value in Strapi using the Strapi Module's service.

In the compensation function, you delete all the created option values in Strapi if an error occurs during the workflow's execution.

#### updateProductOptionValuesMetadataStep

The `updateProductOptionValuesMetadataStep` stores the Strapi IDs of the created product option values in the `metadata` property of the corresponding product option values in Medusa. This allows you to reference the Strapi option values later, such as when updating or deleting them.

To create the step, create the file `src/workflows/steps/update-product-option-values-metadata.ts` with the following content:

```ts title="src/workflows/steps/update-product-option-values-metadata.ts" badgeLabel="Medusa application" badgeColor="green"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
import { ProductOptionValueDTO } from "@medusajs/framework/types"

export type UpdateProductOptionValuesMetadataInput = {
  updates: {
    id: string
    strapiId: number
    strapiDocumentId: string
  }[]
}

export const updateProductOptionValuesMetadataStep = createStep(
  "update-product-option-values-metadata",
  async ({ updates }: UpdateProductOptionValuesMetadataInput, { container }) => {
    const productModuleService = container.resolve(Modules.PRODUCT)

    const updatedOptionValues: ProductOptionValueDTO[] = []

    // Fetch original metadata for compensation
    const originalOptionValues = await productModuleService.listProductOptionValues({ 
      id: updates.map((u) => u.id),
    })

    // Update each option value's metadata
    for (const update of updates) {
      const optionValue = originalOptionValues.find((ov) => ov.id === update.id)
      if (optionValue) {

        const updated = await productModuleService.updateProductOptionValues(
          update.id, 
          {
            metadata: {
              ...optionValue.metadata,
              strapi_id: update.strapiId,
              strapi_document_id: update.strapiDocumentId,
            },
          }
        )

        updatedOptionValues.push(updated)

      }
    }

    return new StepResponse(updatedOptionValues, originalOptionValues)
  },
  async (compensationData, { container }) => {
    if (!compensationData) {
      return
    }

    const productModuleService = container.resolve(Modules.PRODUCT)

    // Restore original metadata
    for (const original of compensationData) {
      await productModuleService.updateProductOptionValues(original.id, {
        metadata: original.metadata,
      })
    }
  }
)
```

This step receives an array of option values to update with their corresponding Strapi IDs.

In the step, you resolve the Product Module's service and update each option value's `metadata` property with the Strapi ID and document ID.

In the compensation function, you restore the original metadata of the option values if an error occurs during the workflow's execution.

#### Create Product Options Workflow

Now that you have created the necessary steps, you can create the workflow.

To create the workflow, create the file `src/workflows/create-options-in-strapi.ts` with the following content:

```ts title="src/workflows/create-options-in-strapi.ts" badgeLabel="Medusa application" badgeColor="green" collapsibleLines="1-15" expandButtonLabel="Show Imports"
import { 
  createWorkflow, 
  WorkflowResponse,
  transform,
} from "@medusajs/framework/workflows-sdk"
import { createOptionsInStrapiStep } from "./steps/create-options-in-strapi"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { 
  CreateOptionValuesInStrapiInput, 
  createOptionValuesInStrapiStep,
} from "./steps/create-option-values-in-strapi"
import { 
  updateProductOptionValuesMetadataStep,
} from "./steps/update-product-option-values-metadata"

export type CreateOptionsInStrapiWorkflowInput = {
  ids: string[]
}

export const createOptionsInStrapiWorkflow = createWorkflow(
  "create-options-in-strapi",
  (input: CreateOptionsInStrapiWorkflowInput) => {
    // Fetch the option with all necessary fields
    // including metadata and product metadata
    const { data: options } = useQueryGraphStep({
      entity: "product_option",
      fields: [
        "id",
        "title",
        "product_id",
        "metadata",
        "product.metadata",
        "values.*",
      ],
      filters: {
        id: input.ids,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    // @ts-ignore
    const preparedOptions = transform({ options }, (data) => {
      return data.options.map((option) => ({
        id: option.id,
        title: option.title,
        strapiProductId: Number(option.product?.metadata?.strapi_id),
      }))
    })

    // Pass the prepared option data to the step
    const strapiOptions = createOptionsInStrapiStep({
      options: preparedOptions,
    })
    
    // Extract option values
    const optionValuesData = transform({ options, strapiOptions }, (data) => {
      return data.options.flatMap((option) => {
        return option.values.map((value) => {
          const strapiOption = data.strapiOptions.find(
            (strapiOption) => strapiOption.medusaId === option.id
          )
          if (!strapiOption) {
            return null
          }
          return {
            id: value.id,
            value: value.value,
            strapiOptionId: strapiOption.id,
          }
        })
      })
    })
    
    const strapiOptionValues = createOptionValuesInStrapiStep({
      optionValues: optionValuesData,
    } as CreateOptionValuesInStrapiInput)

    const optionValuesMetadataUpdate = transform({ strapiOptionValues }, (data) => {
      return {
        updates: [
          ...data.strapiOptionValues.map((optionValue) => ({
            id: optionValue.medusaId,
            strapiId: optionValue.id,
            strapiDocumentId: optionValue.documentId,
          })),
        ],
      }
    })

    updateProductOptionValuesMetadataStep(optionValuesMetadataUpdate)

    return new WorkflowResponse({
      strapi_options: strapiOptions,
    })
  }
)
```

You create a workflow using the `createWorkflow` function. It accepts the workflow's unique name as a first parameter.

It accepts a second parameter: a constructor function that holds the workflow's implementation. The function accepts an input object holding the IDs of the product options to create in Strapi.

In the workflow, you:

1. Retrieve the product options in Medusa using the `useQueryGraphStep`.
   - This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve data in Medusa across modules.
2. Prepare the option data to create using [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md).
   - This function allows you to manipulate data in workflows.
3. Create the product options in Strapi using the `createOptionsInStrapiStep`.
4. Prepare the option values to create using `transform`.
5. Create the product option values in Strapi using the `createOptionValuesInStrapiStep`.
6. Prepare the data to update the option values' metadata using `transform`.
7. Update the option values' metadata using the `updateProductOptionValuesMetadataStep`.

A workflow must return an instance of `WorkflowResponse` that accepts the data to return to the workflow's executor.

You'll use this workflow when you implement the create products in Strapi workflow.

In a workflow, you can't manipulate data because Medusa stores an internal representation of the workflow on application startup. Learn more in the [Data Manipulation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) documentation.

### b. Create Product Variants Workflow

Next, you'll create another sub-workflow to handle the creation of product variants in Strapi. You'll use this sub-workflow in the main product creation workflow.

The workflow to create product variants in Strapi has the following steps:

- [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md): Acquire a lock to prevent concurrent creation
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve product variant data.
- [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md): Release the acquired lock

The first, second, and last steps are available out-of-the-box in Medusa. You need to create the rest of the steps.

#### uploadImagesToStrapiStep

The `uploadImagesToStrapiStep` uploads images to Strapi. You'll use it to upload product and variant images.

To create the step, create the file `src/workflows/steps/upload-images-to-strapi.ts` with the following content:

```ts title="src/workflows/steps/upload-images-to-strapi.ts" badgeLabel="Medusa application" badgeColor="green"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { STRAPI_MODULE } from "../../modules/strapi"
import StrapiModuleService from "../../modules/strapi/service"
import { promiseAll } from "@medusajs/framework/utils"

export type UploadImagesToStrapiInput = {
  items: {
    entity_id: string
    url: string
  }[]
}

export const uploadImagesToStrapiStep = createStep(
  "upload-images-to-strapi",
  async ({ items }: UploadImagesToStrapiInput, { container }) => {
    const strapiService: StrapiModuleService = container.resolve(STRAPI_MODULE)

    const uploadedImages: {
      entity_id: string
      image_id: number
    }[] = []

    try {
      for (const item of items) {
        // Upload image to Strapi
        const uploadedImageId = await strapiService.uploadImages([item.url])
        uploadedImages.push({
          entity_id: item.entity_id,
          image_id: uploadedImageId[0],
        })
      }
    } catch (error) {
      // If error occurs, pass all uploaded files to compensation
      return StepResponse.permanentFailure(
        strapiService.formatStrapiError(
          error, 
          "Failed to upload images to Strapi"
        ),
        { uploadedImages }
      )
    }

    return new StepResponse(
      uploadedImages,
      uploadedImages
    )
  },
  async (compensationData, { container }) => {
    if (!compensationData) {
      return
    }

    const strapiService: StrapiModuleService = container.resolve(STRAPI_MODULE)

    await promiseAll(
      compensationData.map(
        (uploadedImage) => strapiService.deleteImage(uploadedImage.image_id)
      )
    )
  }
)
```

The step accepts an array of items, each having the ID of the item that the image is associated with, and the URL of the image to upload.

In the step, you upload each image to Strapi using the Strapi Module's service.

In the compensation function, you delete all the uploaded images in Strapi if an error occurs during the workflow's execution.

#### createVariantsInStrapiStep

The `createVariantsInStrapiStep` creates product variants in Strapi.

To create the step, create the file `src/workflows/steps/create-variants-in-strapi.ts` with the following content:

```ts title="src/workflows/steps/create-variants-in-strapi.ts" badgeLabel="Medusa application" badgeColor="green"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { STRAPI_MODULE } from "../../modules/strapi"
import StrapiModuleService, { Collection } from "../../modules/strapi/service"

export type CreateVariantsInStrapiInput = {
  variants: {
    id: string
    title: string
    sku?: string
    strapiProductId: number
    optionValueIds?: number[]
    imageIds?: number[]
    thumbnailId?: number
  }[]
}

export const createVariantsInStrapiStep = createStep(
  "create-variants-in-strapi",
  async ({ variants }: CreateVariantsInStrapiInput, { container }) => {
    const strapiService: StrapiModuleService = container.resolve(STRAPI_MODULE)

    const results: Record<string, any>[] = []

    try {
      // Process all variants
      for (const variant of variants) {
        // Create variant in Strapi
        const strapiVariant = await strapiService.create(
          Collection.PRODUCT_VARIANTS, 
          {
            medusaId: variant.id,
            title: variant.title,
            sku: variant.sku,
            product: variant.strapiProductId,
            option_values: variant.optionValueIds || [],
            images: variant.imageIds || [],
            thumbnail: variant.thumbnailId,
          }
        )

        results.push(strapiVariant.data)
      }
    } catch (error) {
      // If error occurs during loop,
      // pass results created so far to compensation
      return StepResponse.permanentFailure(
        strapiService.formatStrapiError(
          error, 
          "Failed to create variants in Strapi"
        ),
        { results }
      )
    }

    return new StepResponse(
      results,
      results
    )
  },
  async (compensationData, { container }) => {
    if (!compensationData) {
      return
    }

    const strapiService: StrapiModuleService = container.resolve(STRAPI_MODULE)
    
    // Delete all created variants
    for (const result of compensationData) {
      await strapiService.delete(Collection.PRODUCT_VARIANTS, result.documentId)
    }
  }
)
```

The step receives the product variants to create in Strapi. In the step, you create each variant in Strapi using the Strapi Module's service.

In the compensation function, you delete all the created variants in Strapi if an error occurs during the workflow's execution.

#### updateProductVariantsMetadataStep

The `updateProductVariantsMetadataStep` stores the Strapi IDs of the created product variants in the `metadata` property of the corresponding product variants in Medusa. This allows you to reference the Strapi variants later, such as when updating or deleting them.

To create the step, create the file `src/workflows/steps/update-product-variants-metadata.ts` with the following content:

```ts title="src/workflows/steps/update-product-variants-metadata.ts" badgeLabel="Medusa application" badgeColor="green"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
import { ProductVariantDTO } from "@medusajs/framework/types"

export type UpdateProductVariantsMetadataInput = {
  updates: {
    variantId: string
    strapiId: number
    strapiDocumentId: string
  }[]
}

export const updateProductVariantsMetadataStep = createStep(
  "update-product-variants-metadata",
  async ({ updates }: UpdateProductVariantsMetadataInput, { container }) => {
    const productModuleService = container.resolve(Modules.PRODUCT)

    const updatedVariants: ProductVariantDTO[] = []

    // Fetch original metadata for compensation
    const originalVariants = await productModuleService.listProductVariants({ 
      id: updates.map((u) => u.variantId),
    })

    // Update each variant's metadata
    for (const update of updates) {
      const variant = originalVariants.find((v) => v.id === update.variantId)
      if (variant) {

        const updated = await productModuleService.updateProductVariants(
          update.variantId, 
          {
            metadata: {
              ...variant.metadata,
              strapi_id: update.strapiId,
              strapi_document_id: update.strapiDocumentId,
            },
          }
        )

        updatedVariants.push(updated)

      }
    }

    return new StepResponse(updatedVariants, originalVariants)
  },
  async (compensationData, { container }) => {
    if (!compensationData) {
      return
    }

    const productModuleService = container.resolve(Modules.PRODUCT)

    // Restore original metadata
    for (const original of compensationData) {
      await productModuleService.updateProductVariants(original.id, {
        metadata: original.metadata,
      })
    }
  }
)
```

This step receives an array of variants to update with their corresponding Strapi IDs.

In the step, you resolve the Product Module's service and update each variant's `metadata` property with the Strapi ID and document ID.

In the compensation function, you restore the original metadata of the variants if an error occurs during the workflow's execution.

#### Create Product Variants Workflow

Now that you have created the necessary steps, you can create the workflow.

To create the workflow, create the file `src/workflows/create-variants-in-strapi.ts` with the following content:

```ts title="src/workflows/create-variants-in-strapi.ts" badgeLabel="Medusa application" badgeColor="green" collapsibleLines="1-12" expandButtonLabel="Show Imports"
import { 
  createWorkflow, 
  WorkflowResponse,
  transform,
  when,
} from "@medusajs/framework/workflows-sdk"
import { acquireLockStep, releaseLockStep, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { CreateVariantsInStrapiInput } from "./steps/create-variants-in-strapi"
import { createVariantsInStrapiStep } from "./steps/create-variants-in-strapi"
import { uploadImagesToStrapiStep } from "./steps/upload-images-to-strapi"
import { updateProductVariantsMetadataStep } from "./steps/update-product-variants-metadata"

export type CreateVariantsInStrapiWorkflowInput = {
  ids: string[]
  productId: string
}

export const createVariantsInStrapiWorkflow = createWorkflow(
  "create-variants-in-strapi",
  (input: CreateVariantsInStrapiWorkflowInput) => {
    acquireLockStep({
      key: ["strapi-product-create", input.productId],
    })
    // Fetch the variant with all necessary fields including option values
    const { data: variants } = useQueryGraphStep({
      entity: "product_variant",
      fields: [
        "id",
        "title",
        "sku",
        "product_id",
        "product.metadata",
        "product.options.id",
        "product.options.values.id",
        "product.options.values.value",
        "product.options.values.metadata",
        "product.strapi_product.*",
        "images.*",
        "thumbnail",
        "options.*",
      ],
      filters: {
        id: input.ids,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const strapiVariants = when({ 
      variants,
    }, (data) => !!(data.variants[0].product as any)?.strapi_product)
      .then(() => {
        const variantImages = transform({ 
          variants,
        }, (data) => {
          return data.variants.flatMap((variant) => variant.images?.map(
            (image) => ({
              entity_id: variant.id,
              url: image.url,
            })
          ) || [])
        })
        const variantThumbnail = transform({ 
          variants,
        }, (data) => {
          return data.variants
          // @ts-ignore
          .filter((variant) => !!variant.thumbnail)
          .flatMap((variant) => ({
            entity_id: variant.id,
            // @ts-ignore
            url: variant.thumbnail!,
          }))
        })
    
        const strapiVariantImages = uploadImagesToStrapiStep({
          items: variantImages,
        })
    
        const strapiVariantThumbnail = uploadImagesToStrapiStep({
          items: variantThumbnail,
        }).config({ name: "upload-variant-thumbnail" })
    
        const variantsData = transform({ 
          variants, 
          strapiVariantImages, 
          strapiVariantThumbnail,
        }, (data) => {
          const varData = data.variants.map((variant) => ({
            id: variant.id,
            title: variant.title,
            sku: variant.sku,
            strapiProductId: Number(variant.product?.metadata?.strapi_id),
            strapiVariantImages: data.strapiVariantImages
              .filter((image) => image.entity_id === variant.id)
              .map((image) => image.image_id),
            strapiVariantThumbnail: data.strapiVariantThumbnail
              .find((image) => image.entity_id === variant.id)?.image_id,
            optionValueIds: variant.options.flatMap((option) => {
              // find the strapi option value id for the option value
              return variant.product?.options.flatMap(
                (productOption) => productOption.values.find(
                  (value) => value.value === option.value
                )?.metadata?.strapi_id).filter((value) => value !== undefined)
            }),
          }))

          return varData
        })
    
        const strapiVariants = createVariantsInStrapiStep({
          variants: variantsData,
        } as CreateVariantsInStrapiInput)
    
        const variantsMetadataUpdate = transform({ strapiVariants }, (data) => {
          return {
            updates: data.strapiVariants.map((strapiVariant) => ({
              variantId: strapiVariant.medusaId,
              strapiId: strapiVariant.id,
              strapiDocumentId: strapiVariant.documentId,
            })),
          }
        })
    
        updateProductVariantsMetadataStep(variantsMetadataUpdate)

        return strapiVariants
      })

    releaseLockStep({
      key: ["strapi-product-create", input.productId],
    })

    return new WorkflowResponse({
      variants: strapiVariants,
    })
  }
)
```

The workflow receives the IDs of the product variants to create in Strapi and the Medusa product ID they belong to.

In the workflow, you:

1. Acquire a [lock](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/locking/index.html.md) to prevent concurrent creation of variants for the same product. This is necessary to handle both the product and variant creation events without duplications.
2. Retrieve the product variants in Medusa using the `useQueryGraphStep`.
3. Check if the product has been created in Strapi using [when](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md). If so, you:
   - Prepare the variant images to upload using `transform`.
   - Prepare the variant thumbnail to upload using `transform`.
   - Upload the variant images to Strapi using the `uploadImagesToStrapiStep`.
   - Upload the variant thumbnail to Strapi using the `uploadImagesToStrapiStep`.
   - Prepare the variant data to create using `transform`.
   - Create the product variants in Strapi using the `createVariantsInStrapiStep`.
   - Prepare the data to update the variants' metadata using `transform`.
   - Update the variants' metadata using the `updateProductVariantsMetadataStep`.
4. Release the acquired lock.

In a workflow, you can't perform steps based on conditions because Medusa stores an internal representation of the workflow on application startup. Learn more in the [Conditions in Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) documentation.

### c. Create Product Creation Workflow

Now that you have created the necessary sub-workflows, you can create the main workflow to handle product creation in Strapi.

The workflow to create products in Strapi has the following steps:

- [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md): Acquire a lock to prevent concurrent creation of product variants
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve product data.
- [uploadImagesToStrapiStep](#uploadImagesToStrapiStep): Upload product images to Strapi.
- [createProductInStrapiStep](#createProductInStrapiStep): Create the product in Strapi.
- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md): Update the product's \`metadata\` with the Strapi ID.
- [createOptionsInStrapiWorkflow](#createOptionsInStrapiWorkflow): Create the product options in Strapi.
- [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md): Release the acquired lock
- [createVariantsInStrapiWorkflow](#createVariantsInStrapiWorkflow): Create the product variants in Strapi.

You only need to create the `createProductInStrapiStep` step. The rest of the steps and workflows are either available out-of-the-box in Medusa or you have already created them.

#### createProductInStrapiStep

The `createProductInStrapiStep` creates a product in Strapi.

To create the step, create the file `src/workflows/steps/create-product-in-strapi.ts` with the following content:

```ts title="src/workflows/steps/create-product-in-strapi.ts" badgeLabel="Medusa application" badgeColor="green"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { STRAPI_MODULE } from "../../modules/strapi"
import StrapiModuleService, { Collection } from "../../modules/strapi/service"

export type CreateProductInStrapiInput = {
  product: {
    id: string
    title: string
    subtitle?: string
    description?: string
    handle: string
    imageIds?: number[]
    thumbnailId?: number
  }
}

export const createProductInStrapiStep = createStep(
  "create-product-in-strapi",
  async ({ product }: CreateProductInStrapiInput, { container }) => {
    const strapiService: StrapiModuleService = container.resolve(STRAPI_MODULE)

    // Create product in Strapi
    const strapiProduct = await strapiService.create(Collection.PRODUCTS, {
      medusaId: product.id,
      title: product.title,
      subtitle: product.subtitle,
      description: product.description,
      handle: product.handle,
      images: product.imageIds || [],
      thumbnail: product.thumbnailId,
    })

    return new StepResponse(
      strapiProduct.data,
      strapiProduct.data
    )
  },
  async (compensationData, { container }) => {
    if (!compensationData) {
      return
    }

    const strapiService: StrapiModuleService = container.resolve(STRAPI_MODULE)

    // Delete the product
    await strapiService.delete(Collection.PRODUCTS, compensationData.documentId)
  }
)
```

The step receives the product to create in Strapi. In the step, you create the product in Strapi using the Strapi Module's service.

In the compensation function, you delete the created product in Strapi if an error occurs during the workflow's execution.

#### Create Product Workflow

Now that you have created the necessary step, you can create the main workflow to handle product creation in Strapi.

To create the workflow, create the file `src/workflows/create-product-in-strapi.ts` with the following content:

```ts title="src/workflows/create-product-in-strapi.ts" badgeLabel="Medusa application" badgeColor="green"
import { 
  createWorkflow, 
  WorkflowResponse,
  transform,
  when,
} from "@medusajs/framework/workflows-sdk"
import { 
  CreateProductInStrapiInput, 
  createProductInStrapiStep,
} from "./steps/create-product-in-strapi"
import { uploadImagesToStrapiStep } from "./steps/upload-images-to-strapi"
import { 
  useQueryGraphStep, 
  updateProductsWorkflow, 
  acquireLockStep, 
  releaseLockStep,
} from "@medusajs/medusa/core-flows"
import { createOptionsInStrapiWorkflow } from "./create-options-in-strapi"
import { createVariantsInStrapiWorkflow } from "./create-variants-in-strapi"

export type CreateProductInStrapiWorkflowInput = {
  id: string
}

export const createProductInStrapiWorkflow = createWorkflow(
  "create-product-in-strapi",
  (input: CreateProductInStrapiWorkflowInput) => {
    acquireLockStep({
      key: ["strapi-product-create", input.id],
      timeout: 60,
    })
    const { data: products } = useQueryGraphStep({
      entity: "product",
      fields: [
        "id",
        "title",
        "subtitle",
        "description",
        "handle",
        "images.url",
        "thumbnail",
        "variants.id",
        "options.id",
      ],
      filters: {
        id: input.id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const productImages = transform({ products }, (data) => {
      return data.products[0].images.map((image) => {
        return {
          entity_id: data.products[0].id,
          url: image.url,
        }
      })
    })

    const strapiProductImages = uploadImagesToStrapiStep({
      items: productImages,
    })

    const strapiProductThumbnail = when(
      ({ products }), 
      // @ts-ignore
      (data) => !!data.products[0].thumbnail
    ).then(() => {
      return uploadImagesToStrapiStep({
        items: [{
          entity_id: products[0].id,
          url: products[0].thumbnail!,
        }],
      }).config({ name: "upload-product-thumbnail" })
    })

    const productWithImages = transform(
      { strapiProductImages, strapiProductThumbnail, products },
      (data) => {
        return {
          id: data.products[0].id,
          title: data.products[0].title,
          subtitle: data.products[0].subtitle,
          description: data.products[0].description,
          handle: data.products[0].handle,
          imageIds: data.strapiProductImages.map((image) => image.image_id),
          thumbnailId: data.strapiProductThumbnail?.[0]?.image_id,
        }
      }
    )

    const strapiProduct = createProductInStrapiStep({
      product: productWithImages,
    } as CreateProductInStrapiInput)

    const productMetadataUpdate = transform({ strapiProduct }, (data) => {
      return {
        selector: { id: data.strapiProduct.medusaId },
        update: {
          metadata: {
            strapi_id: data.strapiProduct.id,
            strapi_document_id: data.strapiProduct.documentId,
          },
        },
      }
    })

    updateProductsWorkflow.runAsStep({
      input: productMetadataUpdate,
    })

    const variantIds = transform({ 
      products,
    }, (data) => data.products[0].variants.map((variant) => variant.id))
    const optionIds = transform({
      products,
    }, (data) => data.products[0].options.map((option) => option.id))

    createOptionsInStrapiWorkflow.runAsStep({
      input: {
        ids: optionIds,
      },
    })

    releaseLockStep({
      key: ["strapi-product-create", input.id],
    })

    createVariantsInStrapiWorkflow.runAsStep({
      input: {
        ids: variantIds,
        productId: input.id,
      },
    })

    return new WorkflowResponse(strapiProduct)
  }
)
```

The workflow receives the ID of the product to create in Strapi.

In the workflow, you:

1. Acquire a [lock](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/locking/index.html.md) to prevent concurrent creation of variants for the same product. This is necessary to handle both the product and variant creation events. Otherwise, variants might be created multiple times.
2. Retrieve the product in Medusa using the `useQueryGraphStep`.
3. Prepare the product images to upload using `transform`.
4. Upload the product images to Strapi using the `uploadImagesToStrapiStep`.
5. Check if the product has a thumbnail using `when`. If so, you upload the thumbnail to Strapi using the `uploadImagesToStrapiStep`.
6. Prepare the product data to create using `transform`.
7. Create the product in Strapi using the `createProductInStrapiStep`.
8. Prepare the data to update the product's metadata using `transform`.
9. Update the product's metadata using the `updateProductsWorkflow`.
10. Prepare the IDs of the product options and variants using `transform`.
11. Create the product options in Strapi using the `createOptionsInStrapiWorkflow`.
12. Release the acquired lock.
13. Create the product variants in Strapi using the `createVariantsInStrapiWorkflow`.

The workflow returns the created Strapi product as a response.

### d. Create Product Created Subscriber

Finally, you need to create a subscriber that listens to the product creation event in Medusa and triggers the `createProductInStrapiWorkflow`.

A subscriber is an asynchronous function that is executed whenever its associated event is emitted.

Refer to the [Subscribers documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) to learn more about subscribers.

To create the subscriber, create the file `src/subscribers/product-created-strapi-sync.ts` with the following content:

```ts title="src/subscribers/product-created-strapi-sync.ts" badgeLabel="Medusa application" badgeColor="green"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createProductInStrapiWorkflow } from "../workflows/create-product-in-strapi"

export default async function productCreatedStrapiSyncHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await createProductInStrapiWorkflow(container).run({
    input: {
      id: data.id,
    },
  })
}

export const config: SubscriberConfig = {
  event: "product.created",
}
```

A subscriber file must export:

- An asynchronous function, which is the subscriber function that is executed when the event is emitted.
- A configuration object that defines the event the subscriber listens to, which is `product.created` in this case.

In the subscriber function, you run the `createProductInStrapiWorkflow`, passing the ID of the created product as input.

### Test Product Creation

Now that you have implemented the product creation workflow and subscriber, you can test the integration.

First, run the following command in the Strapi application's directory to start the Strapi server:

```bash npm2yarn badgeLabel="Strapi" badgeColor="orange"
npm run develop
```

Then, run the following command in the Medusa application's directory to start the Medusa server:

```bash npm2yarn
npm run dev
```

Next, open the Medusa Admin dashboard and [create a new product](https://docs.medusajs.com/user-guide/products/create/index.html.md). Once you create the product, you'll see the following in the Medusa server logs:

```bash
info:    Processing product.created which has 1 subscribers
```

This indicates that the subscriber has been triggered.

Then, open the Strapi Admin dashboard and navigate to the Products collection. You should see the newly created product in the list.

***

## Step 6: Handle Strapi Product Updates

Next, you'll handle product updates in Strapi and synchronize the changes back to Medusa. You'll create a workflow to update the relevant product data in Medusa based on the data received from Strapi.

Then, you'll create an API route webhook that Strapi can call whenever product data is updated. With this setup, you'll have two-way synchronization between Medusa and Strapi for product data.

### a. Handle Strapi Webhook Workflow

The workflow to handle Strapi webhooks has the following steps:

- [prepareStrapiUpdateDataStep](#prepareStrapiUpdateDataStep): Prepare the product update data from the Strapi webhook payload.

You only need to create the `prepareStrapiUpdateDataStep`, `clearProductCacheStep`, and `updateProductOptionValueStep` steps. The rest of the steps and workflows are available out-of-the-box in Medusa.

#### prepareStrapiUpdateDataStep

The `prepareStrapiUpdateDataStep` extracts the data to update from the Strapi webhook payload.

To create the step, create the file `src/workflows/steps/prepare-strapi-update-data.ts` with the following content:

```ts title="src/workflows/steps/prepare-strapi-update-data.ts" badgeLabel="Medusa application" badgeColor="green"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

export const prepareStrapiUpdateDataStep = createStep(
  "prepare-strapi-update-data",
  async ({ entry }: { entry: any }) => {
    let data: Record<string, unknown> = {}
    const model = entry.model

    switch (model) {
      case "product":
        data = {
          id: entry.entry.medusaId,
          title: entry.entry.title,
          subtitle: entry.entry.subtitle,
          description: entry.entry.description,
          handle: entry.entry.handle,
        }
        break
      case "product-variant":
        data = {
          id: entry.entry.medusaId,
          title: entry.entry.title,
          sku: entry.entry.sku,
        }
        break
      case "product-option":
        data = {
          selector: {
            id: entry.entry.medusaId,
          },
          update: {
            title: entry.entry.title,
          },
        }
        break
      case "product-option-value":
        data = {
          optionValueId: entry.entry.medusaId,
          value: entry.entry.value,
        }
        break
    }

    return new StepResponse({ data, model })
  }
)
```

The step receives the Strapi webhook payload containing the updated entry.

In the step, you extract the relevant data based on the model type (product, product variant, product option, or product option value) and return it.

#### clearProductCacheStep

The `clearProductCacheStep` clears the product cache in Medusa to ensure that updated data is served to clients. This is necessary as you'll enable [caching](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching/index.html.md) later, which may cause stale data to be served to the storefront.

To create the step, create the file `src/workflows/steps/clear-product-cache.ts` with the following content:

```ts title="src/workflows/steps/clear-product-cache.ts" badgeLabel="Medusa application" badgeColor="green"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

type ClearProductCacheInput = {
  productId: string | string[]
}

export const clearProductCacheStep = createStep(
  "clear-product-cache",
  async ({ productId }: ClearProductCacheInput, { container }) => {
    const cachingModuleService = container.resolve(Modules.CACHING)

    const productIds = Array.isArray(productId) ? productId : [productId]

    // Clear cache for all specified products
    for (const id of productIds) {
      if (id) {
        await cachingModuleService.clear({
          tags: [`Product:${id}`],
        })
      }
    }

    return new StepResponse({})
  }
)
```

The step receives the ID or IDs of the products to clear the cache for.

In the step, you clear the cache for each specified product using the Caching Module's service.

#### updateProductOptionValueStep

The `updateProductOptionValueStep` updates product option values in Medusa based on the data received from Strapi.

To create the step, create the file `src/workflows/steps/update-product-option-value.ts` with the following content:

```ts title="src/workflows/steps/update-product-option-value.ts" badgeLabel="Medusa application" badgeColor="green"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
import { IProductModuleService } from "@medusajs/framework/types"

type UpdateProductOptionValueInput = {
  id: string
  value: string
}

export const updateProductOptionValueStep = createStep(
  "update-product-option-value",
  async ({ id, value }: UpdateProductOptionValueInput, { container }) => {
    const productModuleService: IProductModuleService = container.resolve(
      Modules.PRODUCT
    )

    // Store the old value for compensation
    const oldOptionValue = await productModuleService
      .retrieveProductOptionValue(id)

    // Update the option value
    const updatedOptionValue = await productModuleService
      .updateProductOptionValues(
        id,
        {
          value,
        }
      )

    return new StepResponse(updatedOptionValue, oldOptionValue)
  },
  async (compensateData, { container }) => {
    if (!compensateData) {
      return
    }

    const productModuleService: IProductModuleService = container.resolve(
      Modules.PRODUCT
    )

    // Revert the option value to its old value
    await productModuleService.updateProductOptionValues(
      compensateData.id,
      {
        value: compensateData.value,
      }
    )
  }
)
```

The step receives the ID of the option value to update and the new value.

In the step, you resolve the Product Module's service and update the option value in Medusa.

In the compensation function, you revert the option value to its old value if an error occurs during the workflow's execution.

#### Handle Strapi Webhook Workflow

Now that you have created the necessary steps, you can create the workflow to handle Strapi webhooks.

To create the workflow, create the file `src/workflows/handle-strapi-webhook.ts` with the following content:

```ts title="src/workflows/handle-strapi-webhook.ts" badgeLabel="Medusa application" badgeColor="green" collapsibleLines="1-19" expandButtonLabel="Show Imports"
import { 
  createWorkflow, 
  when,
  transform,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { prepareStrapiUpdateDataStep } from "./steps/prepare-strapi-update-data"
import { clearProductCacheStep } from "./steps/clear-product-cache"
import { updateProductOptionValueStep } from "./steps/update-product-option-value"
import { 
  updateProductsWorkflow, 
  updateProductVariantsWorkflow, 
  updateProductOptionsWorkflow,
} from "@medusajs/medusa/core-flows"
import { 
  UpsertProductDTO, 
  UpsertProductVariantDTO,
} from "@medusajs/framework/types"

export type WorkflowInput = {
  entry: any
}

export const handleStrapiWebhookWorkflow = createWorkflow(
  "handle-strapi-webhook-workflow",
  (input: WorkflowInput) => {
    const preparedData = prepareStrapiUpdateDataStep({
      entry: input.entry,
    })

    when(input, (input) => input.entry.model === "product")
      .then(() => {
        updateProductsWorkflow.runAsStep({
          input: {
            products: [preparedData.data as unknown as UpsertProductDTO],
          },
        })

        // Clear the product cache after update
        const productId = transform({ preparedData }, (data) => {
          return (data.preparedData.data as any).id
        })

        clearProductCacheStep({ productId })
      })

    when(input, (input) => input.entry.model === "product-variant")
      .then(() => {
        const variants = updateProductVariantsWorkflow.runAsStep({
          input: {
            product_variants: [
              preparedData.data as unknown as UpsertProductVariantDTO,
            ],
          },
        })

        clearProductCacheStep({ 
          productId: variants[0].product_id!,
        }).config({ name: "clear-product-cache-variant" })
      })

    when(input, (input) => input.entry.model === "product-option")
      .then(() => {
        const options = updateProductOptionsWorkflow.runAsStep({
          input: preparedData.data as any,
        })

        clearProductCacheStep({ 
          productId: options[0].product_id!,
        }).config({ name: "clear-product-cache-option" })
      })

    when(input, (input) => input.entry.model === "product-option-value")
      .then(() => {
        // Update the option value using the Product Module
        const optionValueData = transform({ preparedData }, (data) => ({
          id: data.preparedData.data.optionValueId as string,
          value: data.preparedData.data.value as string,
        }))

        updateProductOptionValueStep(optionValueData)

        // Find all variants that use this option value to
        // clear their product cache
        const { data: variants } = useQueryGraphStep({
          entity: "product_variant",
          fields: [
            "id",
            "product_id",
          ],
          filters: {
            options: {
              id: preparedData.data.optionValueId as string,
            },
          },
        }).config({ name: "get-variants-from-option-value" })

        // Clear the product cache for all affected products
        const productIds = transform({ variants }, (data) => {
          const uniqueProductIds = [
            ...new Set(data.variants.map((v) => v.product_id)),
          ]
          return uniqueProductIds as string[]
        })

        clearProductCacheStep({ 
          productId: productIds,
        }).config({ name: "clear-product-cache-option-value" })
      })
  }
)
```

The workflow receives the Strapi webhook payload containing the updated entry.

In the workflow, you:

1. Prepare the update data using the `prepareStrapiUpdateDataStep`.
2. Check if the updated model is a product using `when`. If so, you:
   - Update the product in Medusa using the `updateProductsWorkflow`.
   - Clear the product cache using the `clearProductCacheStep`.
3. Check if the updated model is a product variant using `when`. If so, you
   - Update the product variant in Medusa using the `updateProductVariantsWorkflow`.
   - Clear the product cache using the `clearProductCacheStep`.
4. Check if the updated model is a product option using `when`. If so, you:
   - Update the product option in Medusa using the `updateProductOptionsWorkflow`.
   - Clear the product cache using the `clearProductCacheStep`.
5. Check if the updated model is a product option value using `when`. If so, you:
   - Update the product option value in Medusa using the `updateProductOptionValueStep`.
   - Retrieve all product variants that use the updated option value using the `useQueryGraphStep`.
   - Clear the product cache for all affected products using the `clearProductCacheStep`.

### b. Create Strapi Webhook API Route

Next, you need to create an API route webhook that Strapi can call whenever product data is updated.

An [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) is an endpoint that exposes business logic and commerce features to clients.

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

Refer to the [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) to learn more about them.

To create the API route, create the file `src/api/webhooks/strapi/route.ts` with the following content:

```ts title="src/api/webhooks/strapi/route.ts" badgeLabel="Medusa application" badgeColor="green"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { simpleHash, Modules } from "@medusajs/framework/utils"
import { 
  handleStrapiWebhookWorkflow, 
  WorkflowInput,
} from "../../../workflows/handle-strapi-webhook"

export const POST = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const body = req.body as Record<string, unknown>
  const logger = req.scope.resolve("logger")
  const cachingService = req.scope.resolve(Modules.CACHING)
  
  // Generate a hash of the webhook payload to detect duplicates
  const payloadHash = simpleHash(JSON.stringify(body))
  const cacheKey = `strapi-webhook:${payloadHash}`
  
  // Check if we've already processed this webhook
  const alreadyProcessed = await cachingService.get({ key: cacheKey })
  
  if (alreadyProcessed) {
    logger.debug(`Webhook already processed (hash: ${payloadHash}), skipping to prevent infinite loop`)
    res.status(200).send("OK - Already processed")
    return
  }
  
  if (body.event === "entry.update") {
    const entry = body.entry as Record<string, unknown>
    const entityCacheKey = `strapi-update:${body.model}:${entry.medusaId}`
    await cachingService.set({
      key: entityCacheKey,
      data: { status: "processing", timestamp: Date.now() },
      ttl: 10,
    })
    
    await handleStrapiWebhookWorkflow(req.scope).run({
      input: {
        entry: body,
      } as WorkflowInput,
    })
    
    // Cache the hash to prevent reprocessing (TTL: 60 seconds)
    await cachingService.set({
      key: cacheKey,
      data: { status: "processed", timestamp: Date.now() },
      ttl: 60,
    })
    logger.debug(`Webhook processed and cached (hash: ${payloadHash})`)
  }

  res.status(200).send("OK")
}
```

Since you export `POST` function, you expose a `POST` API route at `/webhooks/strapi`.

In the API route, you:

1. Retrieve the webhook payload from the request body.
2. Resolve the Caching Module's service.
3. Generate a hash of the webhook payload to detect duplicate webhook calls. This is necessary since you've implemented two-way synchronization between Medusa and Strapi, which may lead to infinite loops of updates.
4. If the hash exists in the cache, the webhook has already been processed, so you skip further processing and return a `200` response.
5. If the webhook event is `entry.update`, you:
   - Cache the entity being updated to prevent concurrent updates.
   - Run the `handleStrapiWebhookWorkflow`, passing the webhook payload as input.
   - Cache the hash of the webhook payload to prevent reprocessing for 60 seconds.

### c. Add Webhook Validation Middleware

To ensure that webhook requests are coming from your Strapi application, you'll add a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) that validates the webhook requests.

To add the middleware, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" badgeLabel="Medusa application" badgeColor="green"
import { 
  defineMiddlewares, 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/webhooks/strapi",
      middlewares: [
        async (
          req: MedusaRequest,
          res: MedusaResponse,
          next: MedusaNextFunction
        ) => {
          const apiKeyModuleService = req.scope.resolve(
            Modules.API_KEY
          )
          
          // Extract Bearer token from Authorization header
          const authHeader = req.headers["authorization"]
          const apiKey = authHeader?.replace("Bearer ", "")
          
          if (!apiKey) {
            return res.status(401).json({
              message: "Unauthorized: Missing API key",
            })
          }
          
          try {
            // Validate the API key using Medusa's API Key Module
            const isValid = await apiKeyModuleService.authenticate(apiKey)
            
            if (!isValid) {
              return res.status(401).json({
                message: "Unauthorized: Invalid API key",
              })
            }
            
            // API key is valid, proceed to route handler
            next()
          } catch (error) {
            return res.status(401).json({
              message: "Unauthorized: API key authentication failed",
            })
          }
        },
      ],
    },
  ],
})
```

The middleware file must create middlewares with the `defineMiddlewares` function.

You define a middleware for the `/webhooks/strapi` route that:

1. Resolves the [API Key Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/index.html.md)'s service.
2. Extracts the API key from the `Authorization` header.
3. If the API key is missing, returns a `401 Unauthorized` response.
4. Validates the API key using the API Key Module's service.
5. If the API key is invalid, returns a `401 Unauthorized` response.
6. Otherwise, calls the `next` function to proceed to the route handler.

### d. Enable Caching in Medusa

### Prerequisites

- [Redis installed and Redis server running](https://redis.io/docs/getting-started/installation/)

The [Caching Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/caching/index.html.md) is currently guarded by a feature flag. To enable it, add the feature flag and module in your `medusa-config.ts` file:

```ts title="medusa-config.ts" badgeLabel="Medusa application" badgeColor="green"
module.exports = defineConfig({
  // ...
  modules: [
    // ...
    {
      resolve: "@medusajs/medusa/caching",
      options: {
        providers: [
          {
            resolve: "@medusajs/caching-redis",
            id: "caching-redis",
            options: {
              redisUrl: process.env.REDIS_URL,
            },
          },
        ],
      },
    },
  ],
  featureFlags: {
    caching: true,
  },
})
```

This configuration enables the Caching Module with Redis as the caching provider. Make sure to set the `REDIS_URL` environment variable to point to your Redis server:

```bash
REDIS_URL=redis://localhost:6379
```

You can now use the Caching Module's service in your workflows and API routes. Medusa will also cache product and cart data automatically to improve performance.

### e. Webhook Handling Preparation

Before you test the webhook handling, you need to create a secret API key in Medusa, then configure webhooks in Strapi.

Make sure to start both the Medusa and Strapi servers if they are not already running.

#### Create Secret API Key in Medusa

To create the secret API key in Medusa:

1. Open the Medusa Admin dashboard.
2. Go to Settings -> Secret API Keys.
3. Click on the "Create" button at the top right.
4. Enter a name for the API key. For example, "Strapi".
5. Click on the "Save" button.
6. Copy the generated API key. You'll need it to configure the webhook in Strapi.

![Medusa Admin dashboard with the Secret API Keys page showing the Strapi API key](https://res.cloudinary.com/dza7lstvk/image/upload/v1763368257/Medusa%20Resources/CleanShot_2025-11-17_at_10.20.28_2x_b6cilm.png)

#### Configure Webhook in Strapi

Next, you need to configure a webhook in Strapi to call the Medusa webhook API route whenever product data is updated.

To configure the webhook in Strapi:

1. Open the Strapi Admin dashboard.
2. Go to Settings -> Webhooks.
3. Click on the "Create new webhook" button at the top right.
4. In the webhook creation form:
   - **Name**: Enter a name for the webhook. For example, "Medusa".
   - **URL**: Enter the URL of the Medusa webhook API route. It should be `http://localhost:9000/webhooks/strapi` if you're running Medusa locally.
   - **Headers**: Add a new header with the key `Authorization` and the value `Bearer YOUR_SECRET_API_KEY`. Replace `YOUR_SECRET_API_KEY` with the API key you created in Medusa.
   - **Events**: Select the "Update" event for "Entry". This ensures that the webhook is triggered whenever an entry is updated in Strapi.
5. Click on the "Save" button to create the webhook.

![Strapi Webhook Creation form](https://res.cloudinary.com/dza7lstvk/image/upload/v1763368257/Medusa%20Resources/CleanShot_2025-11-17_at_10.30.22_2x_zohl1q.png)

### Test Strapi Webhook Handling

To test out the webhook handling:

1. Make sure both the Medusa and Strapi servers are running.
2. On the Strapi Admin dashboard, go to Content Manager -> Products.
3. Select an existing product to edit.
4. Update the product's title or description.
5. Click on the "Save" button to save the changes.

Once you save the changes, Strapi will send a webhook to Medusa. You should see the following in the Medusa server logs:

```bash
http:    POST /webhooks/strapi ← - (200) - 153.264 ms
```

This indicates that the webhook was received and processed successfully.

You can also check the product in the Medusa Admin dashboard to verify that the changes made in Strapi are reflected in Medusa.

***

## Step 7: Show Strapi Data in Storefront

Now that you've integrated Strapi with Medusa, you can customize the Next.js Starter Storefront to display product content from Strapi, allowing you to show product content and assets optimized for the storefront.

In this step, you'll customize the Next.js Starter Storefront to show the Strapi product data.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-strapi`, you can find the storefront by going back to the parent directory and changing to the `medusa-strapi-storefront` directory:

```bash
cd ../medusa-strapi-storefront # change based on your project name
```

### a. Retrieve Strapi Product Data

Since you've created a [virtual read-only link](#step-4-create-virtual-read-only-link-to-strapi-products) to Strapi products in Medusa, you can retrieve Strapi product data when retrieving Medusa products.

To retrieve Strapi product data, open `src/lib/data/product.ts`, and add `*strapi_product` to the `fields` query parameter passed in the `listProducts` function:

```ts title="src/lib/data/product.ts" badgeLabel="Storefront" badgeColor="blue" highlights={[["16"]]}
export const listProducts = async ({
  // ...
}: {
  // ...
}): Promise<{
  // ...
}> => {
  // ...

  return sdk.client
    .fetch<{ products: HttpTypes.StoreProduct[]; count: number }>(
      `/store/products`,
      {
        query: {
          fields:
            "*variants.calculated_price,+variants.inventory_quantity,*variants.images,+metadata,+tags,*strapi_product",
          // ...
        },
        // ...
      }
    )
  // ...
}
```

The Strapi product data will now be included in the `strapi_product` property of each Medusa product.

### b. Define Strapi Product Types

Next, you'll define types for the Strapi product data to use in the storefront.

Create the file `src/types/strapi.ts` with the following content:

```ts title="src/types/strapi.ts" badgeLabel="Storefront" badgeColor="blue"
export interface StrapiMedia {
  id: number
  url: string
  alternativeText?: string
  caption?: string
  width?: number
  height?: number
  formats?: {
    thumbnail?: { url: string; width: number; height: number }
    small?: { url: string; width: number; height: number }
    medium?: { url: string; width: number; height: number }
    large?: { url: string; width: number; height: number }
  }
}

export interface StrapiProductOptionValue {
  id: number
  medusaId: string
  value: string
  locale: string
  option?: StrapiProductOption
  variants?: StrapiProductVariant[]
}

export interface StrapiProductOption {
  id: number
  medusaId: string
  title: string
  locale: string
  product?: StrapiProduct
  values?: StrapiProductOptionValue[]
}

export interface StrapiProductVariant {
  id: number
  medusaId: string
  title: string
  sku?: string
  locale: string
  product?: StrapiProduct
  option_values?: StrapiProductOptionValue[]
  images?: StrapiMedia[]
  thumbnail?: StrapiMedia
}

export interface StrapiProduct {
  id: number
  medusaId: string
  title: string
  subtitle?: string
  description?: string
  handle: string
  images?: StrapiMedia[]
  thumbnail?: StrapiMedia
  locale: string
  variants?: StrapiProductVariant[]
  options?: StrapiProductOption[]
}
```

You define types for Strapi media, product option values, product options, product variants, and products.

### c. Add Strapi Product Utilities

Next, add utilities that will allow you to easily retrieve Strapi product data from a product object.

Create the file `src/lib/util/strapi.ts` with the following content:

```ts title="src/lib/util/strapi.ts" badgeLabel="Storefront" badgeColor="blue"
import { HttpTypes } from "@medusajs/types"
import {
  StrapiProduct,
  StrapiMedia,
} from "../../types/strapi"

/**
 - Get Strapi product data from a Medusa product
 */
export function getStrapiProduct(
  product: HttpTypes.StoreProduct
): StrapiProduct | undefined {
  return (product as any).strapi_product as StrapiProduct | undefined
}

/**
 - Get product title from Strapi, fallback to Medusa
 */
export function getProductTitle(
  product: HttpTypes.StoreProduct
): string {
  const strapiProduct = getStrapiProduct(product)
  return strapiProduct?.title || product.title || ""
}

/**
 - Get product subtitle from Strapi
 */
export function getProductSubtitle(
  product: HttpTypes.StoreProduct
): string | undefined {
  const strapiProduct = getStrapiProduct(product)
  return strapiProduct?.subtitle
}

/**
 - Get product description from Strapi, fallback to Medusa
 */
export function getProductDescription(
  product: HttpTypes.StoreProduct
): string | null {
  const strapiProduct = getStrapiProduct(product)
  if (strapiProduct?.description) {
    // Strapi richtext is typically stored as a string or structured data
    // For now, we'll handle it as a string. You may need to parse it based on your Strapi configuration
    return typeof strapiProduct.description === "string"
      ? strapiProduct.description
      : JSON.stringify(strapiProduct.description)
  }
  return product.description
}

/**
 - Get product thumbnail from Strapi, fallback to Medusa
 */
export function getProductThumbnail(
  product: HttpTypes.StoreProduct
): string | null {
  const strapiProduct = getStrapiProduct(product)
  
  if (strapiProduct?.thumbnail?.url) {
    return strapiProduct.thumbnail.url
  }
  
  return product.thumbnail || null
}

/**
 - Get product images from Strapi, fallback to Medusa
 */
export function getProductImages(
  product: HttpTypes.StoreProduct
): HttpTypes.StoreProductImage[] {
  const strapiProduct = getStrapiProduct(product)
  
  if (strapiProduct?.images && strapiProduct.images.length > 0) {
    // Convert Strapi media to Medusa product image format
    return strapiProduct.images.map((image: StrapiMedia, index: number) => ({
      id: image.id.toString(),
      url: image.url,
      metadata: {
        alt: image.alternativeText || `Product image ${index + 1}`,
      },
      rank: index + 1,
    })) as HttpTypes.StoreProductImage[]
  }
  
  return product.images || []
}

/**
 - Get variant title from Strapi, fallback to Medusa
 */
export function getVariantTitle(
  variant: HttpTypes.StoreProductVariant,
  product: HttpTypes.StoreProduct
): string {
  const strapiProduct = getStrapiProduct(product)
  const strapiVariant = strapiProduct?.variants?.find(
    (v) => v.medusaId === variant.id
  )
  return strapiVariant?.title || variant.title || ""
}

/**
 - Get option title from Strapi, fallback to Medusa
 */
export function getOptionTitle(
  option: HttpTypes.StoreProductOption,
  product: HttpTypes.StoreProduct
): string {
  const strapiProduct = getStrapiProduct(product)
  const strapiOption = strapiProduct?.options?.find(
    (o) => o.medusaId === option.id
  )
  return strapiOption?.title || option.title || ""
}

/**
 - Get option value text from Strapi, fallback to Medusa
 */
export function getOptionValueText(
  optionValue: { id: string; option_id: string; value: string },
  product: HttpTypes.StoreProduct
): string {
  const strapiProduct = getStrapiProduct(product)
  const strapiOption = strapiProduct?.options?.find(
    (o) => o.medusaId === optionValue.option_id
  )
  const strapiOptionValue = strapiOption?.values?.find(
    (v) => v.medusaId === optionValue.id
  )
  return strapiOptionValue?.value || optionValue.value
}

/**
 - Get all option values for a variant with Strapi labels
 */
export function getVariantOptionValues(
  variant: HttpTypes.StoreProductVariant,
  product: HttpTypes.StoreProduct
): Array<{ optionTitle: string; value: string }> {
  if (!variant.options || variant.options.length === 0) {
    return []
  }

  return variant.options
    .filter((opt) => opt.option_id && opt.id)
    .map((opt) => {
      const option = product.options?.find((o) => o.id === opt.option_id)
      const optionTitle = option
        ? getOptionTitle(option, product)
        : ""
      const value = getOptionValueText(
        { id: opt.id, option_id: opt.option_id!, value: opt.value! },
        product
      )
      return { optionTitle, value }
    })
    .filter((opt) => opt.optionTitle && opt.value)
}

/**
 - Get images for a specific variant from Strapi
 */
export function getVariantImages(
  variant: HttpTypes.StoreProductVariant,
  product: HttpTypes.StoreProduct
): HttpTypes.StoreProductImage[] {
  const strapiProduct = getStrapiProduct(product)
  const strapiVariant = strapiProduct?.variants?.find(
    (v) => v.medusaId === variant.id
  )
  
  // If variant has specific images in Strapi, use those
  if (strapiVariant?.images && strapiVariant.images.length > 0) {
    return strapiVariant.images.map((image: StrapiMedia, index: number) => ({
      id: image.id.toString(),
      url: image.url,
      metadata: {
        alt: image.alternativeText || `Variant image ${index + 1}`,
      },
      rank: index + 1,
    })) as HttpTypes.StoreProductImage[]
  }
  
  // Fall back to Medusa variant images
  if ((variant as any).images && (variant as any).images.length > 0) {
    return (variant as any).images
  }
  
  // Finally, fall back to product images
  return getProductImages(product)
}
```

You define the following utilities:

- `getStrapiProduct`: Retrieves the Strapi product data from a Medusa product.
- `getProductTitle`: Retrieves the product title from Strapi, falling back to Medusa if not available.
- `getProductSubtitle`: Retrieves the product subtitle from Strapi.
- `getProductDescription`: Retrieves the product description from Strapi, falling back to Medusa if not available.
- `getProductThumbnail`: Retrieves the product thumbnail from Strapi, falling back to Medusa if not available.
- `getProductImages`: Retrieves the product images from Strapi, falling back to Medusa if not available.
- `getVariantTitle`: Retrieves the variant title from Strapi, falling back to Medusa if not available.
- `getOptionTitle`: Retrieves the option title from Strapi, falling back to Medusa if not available.
- `getOptionValueText`: Retrieves the option value text from Strapi, falling back to Medusa if not available.
- `getVariantOptionValues`: Retrieves all option values for a variant with Strapi labels.
- `getVariantImages`: Retrieves images for a specific variant from Strapi, falling back to Medusa if not available.

### d. Customize Product Preview

Next, you'll customize the product preview component to show Strapi product data. This component is displayed on the product listing page.

In `src/modules/products/components/product-preview/index.tsx`, add the following imports at the top of the file:

```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { 
  getProductTitle, 
  getProductImages, 
  getProductThumbnail,
} from "@lib/util/strapi"
```

Then, in the `ProductPreview` component, define the following variables before the `return` statement:

```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const title = getProductTitle(product)
const images = getProductImages(product)
const thumbnail = getProductThumbnail(product) || product.thumbnail
```

Finally, replace the `return` statement with the following:

```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <LocalizedClientLink href={`/products/${product.handle}`} className="group">
    <div data-testid="product-wrapper">
      <Thumbnail
        thumbnail={thumbnail}
        images={images}
        size="full"
        isFeatured={isFeatured}
      />
      <div className="flex txt-compact-medium mt-4 justify-between">
        <Text className="text-ui-fg-subtle" data-testid="product-title">
          {title}
        </Text>
        <div className="flex items-center gap-x-2">
          {cheapestPrice && <PreviewPrice price={cheapestPrice} />}
        </div>
      </div>
    </div>
  </LocalizedClientLink>
)
```

You make two key changes:

1. Pass the `images` and `thumbnail` variables as props to the `Thumbnail` component to show Strapi product images.
2. Use the `title` variable to display the Strapi product title.

### e. Customize Product Details Metadata

Next, you'll customize the product details component to show Strapi product data.

First, you'll use the Strapi product title, subtitle, and images in the page's metadata.

In `src/app/[countryCode]/(main)/products/[handle]/page.tsx`, add the following imports at the top of the file:

```tsx title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue"
import {
  getProductImages,
  getVariantImages,
  getProductTitle,
  getProductSubtitle,
  getProductThumbnail,
} from "@lib/util/strapi"
import { StrapiMedia } from "../../../../../types/strapi"
```

Then, replace the `getImagesForVariant` function with the following:

```tsx title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue"
function getImagesForVariant(
  product: HttpTypes.StoreProduct,
  selectedVariantId?: string
) {
  // Get Strapi images or fallback to Medusa images
  const productImages = getProductImages(product)

  if (!selectedVariantId || !product.variants) {
    return productImages
  }

  const variant = product.variants!.find((v) => v.id === selectedVariantId)
  if (!variant) {
    return productImages
  }

  // Get variant images from Strapi or fallback to Medusa
  const variantImages = getVariantImages(variant, product)
  
  // If variant has specific images, use those; otherwise use product images
  if (
    variantImages.length > 0 && 
    (variant as any).images && 
    (variant as any).images.length > 0
  ) {
    const imageIdsMap = new Map((variant as any)
      .images.map((i: StrapiMedia) => [i.id, true]))
    return productImages.filter((i) => imageIdsMap.has(i.id))
  }

  return productImages
}
```

This function now retrieves product and variant images from Strapi using the utilities you defined earlier. These images will be shown on the product's details page.

Next, in the `generateMetadata` function, replace the `return` statement with the following:

```tsx title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue"
const title = getProductTitle(product)
const subtitle = getProductSubtitle(product)
const thumbnail = getProductThumbnail(product) || product.thumbnail

return {
  title: `${title} | Medusa Store`,
  description: subtitle || title,
  openGraph: {
    title: `${title} | Medusa Store`,
    description: subtitle || title,
    images: thumbnail ? [thumbnail] : [],
  },
}
```

You use the Strapi product title, subtitle, and thumbnail in the page's metadata.

### f. Customize Product Details Page

Next, you'll customize the product details page to show Strapi product data.

The images for the product details page were already customized in the previous section when you updated the `getImagesForVariant` function.

#### Show Product Title and Description

First, you'll show the Strapi product title and description on the product details page.

Since the product description is in markdown format, you need to install the `react-markdown` package to render it. Run the following command in your storefront directory:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm install react-markdown
```

Then, in `src/modules/products/templates/product-info/index.tsx`, add the following imports at the top of the file:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import {
  getProductTitle,
  getProductDescription,
} from "@lib/util/strapi"
import Markdown from "react-markdown"
```

Next, in the `ProductInfo` component, define the following variables before the `return` statement:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const title = getProductTitle(product)
const description = getProductDescription(product)
```

Finally, in the `return` statement, replace `{product.title}` with `{title}`:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div id="product-info">
    {/* ... */}
    <Heading
      // ...
    >
      {title}
    </Heading>
  </div>
)
```

Then, find the `Text` component wrapping the `{product.description}` and replace it with the following:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
<div
  className="text-medium text-ui-fg-subtle whitespace-pre-line"
  data-testid="product-description"
>
  <Markdown 
    allowedElements={[
      "p", "ul", "ol", "li", "strong", "em", "blockquote", "hr", "br", "a",
    ]}
    unwrapDisallowed
  >
    {description}
  </Markdown>
</div>
```

#### Show Option Titles and Values

Next, you'll show Strapi option titles and values on the product details page.

Replace the content of `src/modules/products/components/product-actions/option-select.tsx` with the following:

```tsx title="src/modules/products/components/product-actions/option-select.tsx" badgeLabel="Storefront" badgeColor="blue"
import { HttpTypes } from "@medusajs/types"
import { clx } from "@medusajs/ui"
import React from "react"
import { getOptionValueText } from "@lib/util/strapi"

type OptionSelectProps = {
  option: HttpTypes.StoreProductOption
  current: string | undefined
  updateOption: (title: string, value: string) => void
  title: string
  product: HttpTypes.StoreProduct
  disabled: boolean
  "data-testid"?: string
}

const OptionSelect: React.FC<OptionSelectProps> = ({
  option,
  current,
  updateOption,
  title,
  product,
  "data-testid": dataTestId,
  disabled,
}) => {
  const filteredOptions = (option.values ?? []).map((v) => ({
    originalValue: v.value,
    displayValue: getOptionValueText(
      { id: v.id, option_id: option.id, value: v.value },
      product
    ),
  }))

  return (
    <div className="flex flex-col gap-y-3">
      <span className="text-sm">Select {title}</span>
      <div
        className="flex flex-wrap justify-between gap-2"
        data-testid={dataTestId}
      >
        {filteredOptions.map(({ originalValue, displayValue }) => {
          return (
            <button
              onClick={() => updateOption(option.id, originalValue)}
              key={originalValue}
              className={clx(
                "border-ui-border-base bg-ui-bg-subtle border text-small-regular h-10 rounded-rounded p-2 flex-1 ",
                {
                  "border-ui-border-interactive": originalValue === current,
                  "hover:shadow-elevation-card-rest transition-shadow ease-in-out duration-150":
                    originalValue !== current,
                }
              )}
              disabled={disabled}
              data-testid="option-button"
            >
              {displayValue}
            </button>
          )
        })}
      </div>
    </div>
  )
}

export default OptionSelect
```

You make the following key changes:

- Add the `product` prop to the `OptionSelect` component.
- Use the `getOptionValueText` utility to get the option value text from Strapi.
- Display the Strapi option value text in the option buttons.

Then, in `src/modules/products/components/product-actions/index.tsx`, add the following import at the top of the file:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { getOptionTitle } from "@lib/util/strapi"
```

And in the `return` statement, find the `product.options` loop and replace it with the following:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <>
    {/* ... */}
    {(product.options || []).map((option) => {
      const optionTitle = getOptionTitle(option, product)
      return (
        <div key={option.id}>
          <OptionSelect
            option={option}
            current={options[option.id]}
            updateOption={setOptionValue}
            title={optionTitle}
            product={product}
            data-testid="product-options"
            disabled={!!disabled || isAdding}
          />
        </div>
      )
    })}
    {/* ... */}
  </>
)
```

You use the `getOptionTitle` utility to get the option title from Strapi and pass the `product` prop to the `OptionSelect` component.

You need to make similar changes in the `src/modules/products/components/product-actions/mobile-actions.tsx` component. First, add the following imports at the top of the file:

```tsx title="src/modules/products/components/product-actions/mobile-actions.tsx" badgeLabel="Storefront" badgeColor="blue"
import { getProductTitle, getOptionTitle } from "@lib/util/strapi"
```

Then, in the `return` statement, replace the `{product.title}` with the following:

```tsx title="src/modules/products/components/product-actions/mobile-actions.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <>
    {/* ... */}
    <span data-testid="mobile-title">{getProductTitle(product)}</span>
    {/* ... */}
  </>
)
```

Then, find the `product.options` loop and replace it with the following:

```tsx title="src/modules/products/components/product-actions/mobile-actions.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <>
    {/* ... */}
    {(product.options || []).map((option) => {
      const optionTitle = getOptionTitle(option, product)
      return (
        <div key={option.id}>
          <OptionSelect
            option={option}
            current={options[option.id]}
            updateOption={updateOptions}
            title={optionTitle}
            product={product}
            disabled={optionsDisabled}
          />
        </div>
      )
    })}
    {/* ... */}
  </>
)
```

You retrieve the Strapi option title and pass the `product` prop to the `OptionSelect` component.

### g. Customize Line Item Options

Finally, you'll customize the line item options to either show Strapi variant titles or option titles and values.

Replace the content of `src/modules/common/components/line-item-options/index.tsx` with the following:

```tsx title="src/modules/common/components/line-item-options/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { HttpTypes } from "@medusajs/types"
import { Text } from "@medusajs/ui"
import { getVariantTitle, getVariantOptionValues } from "@lib/util/strapi"

type LineItemOptionsProps = {
  variant: HttpTypes.StoreProductVariant | undefined
  product?: HttpTypes.StoreProduct
  "data-testid"?: string
  "data-value"?: HttpTypes.StoreProductVariant
}

const LineItemOptions = ({
  variant,
  product,
  "data-testid": dataTestid,
  "data-value": dataValue,
}: LineItemOptionsProps) => {
  if (!variant) {
    return null
  }

  // Get product from variant if not provided
  const productData = product || (variant as any).product

  // Get variant title from Strapi
  const variantTitle = productData
    ? getVariantTitle(variant, productData)
    : variant.title

  // Get option values from Strapi
  const optionValues = productData
    ? getVariantOptionValues(variant, productData)
    : []

  // If we have option values, show them; otherwise show variant title
  if (optionValues.length > 0) {
    const displayText = optionValues
      .map((opt) => `${opt.optionTitle}: ${opt.value}`)
      .join(" / ")

    return (
      <Text
        data-testid={dataTestid}
        data-value={dataValue}
        className="inline-block txt-medium text-ui-fg-subtle w-full overflow-hidden text-ellipsis"
      >
        {displayText}
      </Text>
    )
  }

  return (
    <Text
      data-testid={dataTestid}
      data-value={dataValue}
      className="inline-block txt-medium text-ui-fg-subtle w-full overflow-hidden text-ellipsis"
    >
      Variant: {variantTitle}
    </Text>
  )
}

export default LineItemOptions
```

You make the following key changes:

- Add a `product` prop to the `LineItemOptions` component.
- Use the `getVariantTitle` utility to get the variant title from Strapi.
- Use the `getVariantOptionValues` utility to get the option titles and values from Strapi.
- If option values are available, display them; otherwise, display the variant title.

This component is used in cart and order components to show line item details. So, you need to pass the `product` prop where the component is used.

In `src/modules/cart/components/item/index.tsx`, find the `LineItemOptions` component in the `return` statement and update it as follows:

```tsx title="src/modules/cart/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <Table.Row>
    {/* ... */}
    <LineItemOptions
      variant={item.variant}
      product={item.variant?.product!}
      data-testid="product-variant"
    />
    {/* ... */}
  </Table.Row>
)
```

Next, in `src/modules/layout/components/cart-dropdown/index.tsx`, find the `LineItemOptions` component in the `return` statement and update it as follows:

```tsx title="src/modules/layout/components/cart-dropdown/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div>
    {/* ... */}
    <LineItemOptions
      variant={item.variant}
      product={item.variant?.product!}
      data-testid="cart-item-variant"
      data-value={item.variant}
    />
    {/* ... */}
  </div>
)
```

Finally, in `src/modules/order/components/item/index.tsx`, find the `LineItemOptions` component in the `return` statement and update it as follows:

```tsx title="src/modules/order/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <Table.Row>
    {/* ... */}
    <LineItemOptions
      variant={item.variant}
      product={item.variant?.product!}
      data-testid="product-variant"
    />
    {/* ... */}
  </Table.Row>
)
```

This will show Strapi variant titles or option titles and values in the cart and order line items.

### Test Storefront Customizations

To test the storefront customizations, make sure both the Medusa and Strapi servers are running.

Then, run the following command in the Next.js Starter Storefront directory to start the storefront:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

You can open the storefront in your browser at `http://localhost:8000`.

You'll see the Strapi product data in the following places:

1. Go to Menu -> Store. On the product listing page, you'll see the Strapi product titles and images.
2. Open a product's details page. You'll see the Strapi product title, description, images, option titles, and option values.
3. Add the product to the cart. You'll see the Strapi variant titles or option titles and values in the cart dropdown and cart page.
4. Place an order. You'll see the Strapi variant titles or option titles and values in the order confirmation page.

***

## Step 8: Handle More Product Events

Your setup now supports creating products in Strapi when they're created in Medusa. However, you should also support updating and deleting products and their related models to keep data in sync between systems.

For each product event, such as `product.deleted` or `product-variant.updated`, you need to:

1. Create a workflow that updates or deletes the corresponding data in Strapi using the Strapi Module's service.
2. Create a subscriber that listens for the event and triggers the workflow.

You can find all workflows and subscribers for product events in the [Strapi Integration Repository](https://github.com/medusajs/examples/tree/main/strapi-integration/medusa).

***

## Next Steps

You've successfully integrated Medusa with Strapi to manage content related to products, variants, and options. You can expand this integration by adding more features, such as:

1. Managing the content of other entities, like categories or collections. The process is similar to what you've done for products:
   1. Create a content type in Strapi for the entity.
   2. Create Medusa workflows and subscribers to handle the creation, update, and deletion of the entity.
   3. Display the Strapi data in your Next.js Starter Storefront.
2. Enable [internationalization](https://docs.strapi.io/cms/features/internationalization) in Strapi to support multiple languages:
   - You only need to manage the localized content in Strapi. Only the default locale will be synced with Medusa.
   - You can display the localized content in your Next.js Starter Storefront based on the customer's locale.
3. Add custom fields to the Strapi content types that are relevant to the storefront, such as SEO metadata or promotional banners.

### Learn More about Medusa

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.


# Integrations

You can integrate any third-party service to Medusa, including storage services, notification systems, Content-Management Systems (CMS), etc… By integrating third-party services, you build flows and synchronize data around these integrations, making Medusa not only your commerce application, but a middleware layer between your data sources and operations.

Medusa provides integrations out-of-the-box that are listed here, but you can also create your own integrations, such as integrating ERP systems, as explained in [this guide](https://docs.medusajs.com/docs/learn/customization/integrate-systems/index.html.md).

This section holds guides to help technical teams add integrations to a Medusa application. If you're not a technical user, refer your technical team to this documentation instead.

## Analytics

An Analytics Module Provider tracks events and user behavior in your Medusa application using a third-party service.

- [PostHog](https://docs.medusajs.com/infrastructure-modules/analytics/posthog/index.html.md)
- [Segment](https://docs.medusajs.com/integrations/guides/segment/index.html.md)

Learn how to integrate a custom third-party analytics provider in the [Create Analytics Module Provider](https://docs.medusajs.com/references/analytics/provider/index.html.md) documentation.

***

## Auth

An Auth Module Provider authenticates users with their account on a third-party service.

- [Google](https://docs.medusajs.com/commerce-modules/auth/auth-providers/google/index.html.md)
- [GitHub](https://docs.medusajs.com/commerce-modules/auth/auth-providers/github/index.html.md)
- [Okta](https://docs.medusajs.com/integrations/guides/okta/index.html.md)

Learn how to integrate a custom third-party authentication provider in the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider/index.html.md) documentation.

***

## CMS

Integrate a third-party Content-Management System (CMS) to utilize rich content-related features.

- [Contentful (Localization)](https://docs.medusajs.com/integrations/guides/contentful/index.html.md)
- [Payload CMS](https://docs.medusajs.com/integrations/guides/payload/index.html.md)
- [Sanity](https://docs.medusajs.com/integrations/guides/sanity/index.html.md)
- [Strapi](https://docs.medusajs.com/integrations/guides/strapi/index.html.md)

***

## ERP

Integrate your business's Enterprise Resource Planning (ERP) system with Medusa to sync products and orders, restrict purchase with custom rules, and more.

To learn about the general approach of integrating an ERP with Medusa and the different use cases you can implement, refer to the [ERP Recipe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/erp/index.html.md).

- [Odoo](https://docs.medusajs.com/recipes/erp/odoo/index.html.md)

***

## File

A File Module Provider uploads and manages assets, such as product images, on a third-party service.

- [AWS S3 (and Compatible APIs)](https://docs.medusajs.com/infrastructure-modules/file/s3/index.html.md)

Learn how to integrate a custom third-party file or storage provider in the [Create File Module Provider](https://docs.medusajs.com/references/file-provider-module/index.html.md) documentation.

***

## Fulfillment

A Fulfillment Module Provider provides fulfillment options during checkout, calculates shipping rates, and processes an order's fulfillments.

- [ShipStation](https://docs.medusajs.com/integrations/guides/shipstation/index.html.md)

Learn how to integrate a third-party fulfillment provider in the [Create Fulfillment Module Provider](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) documentation.

***

## Instrumentation

Integrate a third-party service to monitor performance, errors, and other metrics in your Medusa application.

- [Sentry](https://docs.medusajs.com/integrations/guides/sentry/index.html.md)

***

## Migration

Migrate data from another ecommerce platform to Medusa.

- [Magento](https://docs.medusajs.com/integrations/guides/magento/index.html.md)

***

## Notification

A Notification Module Provider sends messages to users and customers in your Medusa application using a third-party service.

- [SendGrid](https://docs.medusajs.com/infrastructure-modules/notification/sendgrid/index.html.md)
- [Mailchimp](https://docs.medusajs.com/integrations/guides/mailchimp/index.html.md)
- [Resend](https://docs.medusajs.com/integrations/guides/resend/index.html.md)
- [Slack](https://docs.medusajs.com/integrations/guides/slack/index.html.md)
- [Twilio SMS](https://docs.medusajs.com/how-to-tutorials/tutorials/phone-auth#step-3-integrate-twilio-sms/index.html.md)

Learn how to integrate a third-party notification provider in the [Create Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md) documentation.

***

## Payment

A Payment Module Provider processes payments made in your Medusa store using a third-party service.

- [Stripe](https://docs.medusajs.com/commerce-modules/payment/payment-provider/stripe/index.html.md)
- [PayPal](https://docs.medusajs.com/integrations/guides/paypal/index.html.md)

Learn how to integrate a third-party payment provider in the [Create Payment Module Provider](https://docs.medusajs.com/references/payment/provider/index.html.md) documentation.

***

## Search

Integrate a search engine to index and search products or other types of data in your Medusa application.

- [Algolia](https://docs.medusajs.com/integrations/guides/algolia/index.html.md)
- [Meilisearch](https://docs.medusajs.com/integrations/guides/meilisearch/index.html.md)

***

## Tax

Integrate a third-party tax calculation service to handle tax rates and rules in your Medusa application.

- [Avalara](https://docs.medusajs.com/integrations/guides/avalara/index.html.md)


# How to Build a Wishlist Plugin

In this guide, you'll learn how to build a wishlist [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) which are available out-of-the-box.

Customers browsing your store may be interested in a product but not ready to buy it yet. They may want to save the product for later or share it with friends and family. A wishlist feature allows customers to save products they like and access them later.

This guide will teach you how to:

- Install and set up a Medusa application project.
- Install and set up a Medusa plugin.
- Implement the wishlist features in the plugin.
  - Features include allowing customers to add products to a wishlist, view and manage their wishlist, and share their wishlist.
- Test and use the wishlist plugin in your Medusa application.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer.

- [Wishlist Plugin Example Repository](https://github.com/medusajs/examples/tree/main/wishlist-plugin): Find the full code for this guide in, with the plugin to install.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1737459635/OpenApi/Wishlist_Postman_gjk7mn.yml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

You'll first install a Medusa application that exposes core commerce features through REST APIs. You'll later install the wishlist plugin in this application to test it out.

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll be asked for the project's name. You can also optionally choose to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).

Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Install a Medusa Plugin Project

A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. You can add in the plugin [API Routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), and other customizations, as you'll see in this guide. Afterward, you can test it out locally in a Medusa application, then publish it to npm to install and use it in any Medusa application.

Learn more about plugins in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md).

A Medusa plugin is set up in a different project, giving you the flexibility in building and publishing it, while providing you with the tools to test it out locally in a Medusa application.

To create a new Medusa plugin project, run the following command in a directory different than that of the Medusa application:

```bash npm2yarn
npx create-medusa-app@latest medusa-plugin-wishlist --plugin
```

Where `medusa-plugin-wishlist` is the name of the plugin's directory and the name set in the plugin's `package.json`. So, if you wish to publish it to NPM later under a different name, you can change it here in the command or later in `package.json`.

Once the installation process is done, a new directory named `medusa-plugin-wishlist` will be created with the plugin project files.

![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg)

***

## Step 3: Set up Plugin in Medusa Application

Before you start your development, you'll set up the plugin in the Medusa application you installed in the first step. This will allow you to test the plugin during your development process.

In the plugin's directory, run the following command to publish the plugin to the local package registry:

```bash title="Plugin project"
npx medusa plugin:publish
```

This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`.

Next, you'll install the plugin in the Medusa application from the local registry.

If you've installed your Medusa project before v2.3.1, you must install [yalc](https://github.com/wclr/yalc) as a development dependency first.

Run the following command in the Medusa application's directory to install the plugin:

```bash title="Medusa application"
npx medusa plugin:add medusa-plugin-wishlist
```

This command installs the plugin in the Medusa application from the local package registry.

Next, register the plugin in the `medusa-config.ts` file of the Medusa application:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  plugins: [
    {
      resolve: "medusa-plugin-wishlist",
      options: {},
    },
  ],
})
```

Finally, to ensure your plugin's changes are constantly published to the local registry, simplifying your testing process, keep the following command running in the plugin project during development:

```bash title="Plugin project"
npx medusa plugin:develop
```

***

## Step 4: Implement Wishlist Module

To add custom tables to the database, which are called data models, you create a module. A module is a package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

While you can create modules outside of a plugin and install them in the Medusa application, plugins allow you to bundle modules with other customizations, such as API routes and workflows.

In this step, you'll create a Wishlist Module within the wishlist plugin. This module adds custom data models for wishlists and their items, which you'll use in later steps to store a customer's wishlist.

Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).

### Create Module Directory

A module is created under the `src/modules` directory of your plugin. So, create the directory `src/modules/wishlist`.

![Diagram showcasing the module directory to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1737461182/Medusa%20Resources/wishlist-1_z3kzfv.jpg)

### Create Data Models

A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

Learn more about data models in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md).

In the Wishlist Module, you'll create two data models: `Wishlist` and `WishlistItem`. The `Wishlist` model represents a customer's wishlist, while the `WishlistItem` model represents a product in the wishlist.

Starting with the `Wishlist` model, create a file `src/modules/wishlist/models/wishlist.ts` with the following content:

![Directory structure after adding the Wishlist model](https://res.cloudinary.com/dza7lstvk/image/upload/v1737461304/Medusa%20Resources/wishlist-2_co2lht.jpg)

```ts title="src/modules/wishlist/models/wishlist.ts"
import { model } from "@medusajs/framework/utils"
import { WishlistItem } from "./wishlist-item"

export const Wishlist = model.define("wishlist", {
  id: model.id().primaryKey(),
  customer_id: model.text(),
  sales_channel_id: model.text(),
  items: model.hasMany(() => WishlistItem, {
    mappedBy: "wishlist",
  }),
})
.indexes([
  {
    on: ["customer_id", "sales_channel_id"],
    unique: true,
  },
])
```

The `Wishlist` model has the following properties:

- `id`: A unique identifier for the wishlist.
- `customer_id`: The ID of the customer who owns the wishlist.
- `sales_channel_id`: The ID of the sales channel where the wishlist is created. In Medusa, product availability can differ between sales channels. This ensures only products available in the customer's sales channel are added to the wishlist.
- `items`: A relation to the `WishlistItem` model, representing the products in the wishlist. You'll add this data model next.

Learn more about data model [properties](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md) and [relations](https://docs.medusajs.com/docs/learn/fundamentals/data-models/relationships/index.html.md).

You also define a unique index on the `customer_id` and `sales_channel_id` columns to ensure a customer can only have one wishlist per sales channel.

Learn more about data model indexes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/index/index.html.md).

Next, create the `WishlistItem` model in the file `src/modules/wishlist/models/wishlist-item.ts`:

![Directory structure after adding the WishlistItem model](https://res.cloudinary.com/dza7lstvk/image/upload/v1737461521/Medusa%20Resources/wishlist-3_fxcjxy.jpg)

```ts title="src/modules/wishlist/models/wishlist-item.ts"
import { model } from "@medusajs/framework/utils"
import { Wishlist } from "./wishlist"

export const WishlistItem = model.define("wishlist_item", {
  id: model.id().primaryKey(),
  product_variant_id: model.text(),
  wishlist: model.belongsTo(() => Wishlist, {
    mappedBy: "items",
  }),
})
.indexes([
  {
    on: ["product_variant_id", "wishlist_id"],
    unique: true,
  },
])
```

The `WishlistItem` model has the following properties:

- `id`: A unique identifier for the wishlist item.
- `product_variant_id`: The ID of the product variant in the wishlist.
- `wishlist`: A relation to the `Wishlist` model, representing the wishlist the item belongs to.

You also define a unique index on the `product_variant_id` and `wishlist_id` columns to ensure a product variant is added to the wishlist only once. The `wishlist_id` column is available as a by-product of the `belongsTo` relation.

### Create Service

You define data-management methods of your data models in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can perform database operations.

Learn more about services in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md).

In this section, you'll create the Wishlist Module's service that's used to manage wishlists and wishlist items. Create the file `src/modules/wishlist/service.ts` with the following content:

![Directory structure after adding the service file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737461698/Medusa%20Resources/wishlist-4_j5ka26.jpg)

```ts title="src/modules/wishlist/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import { Wishlist } from "./models/wishlist"
import { WishlistItem } from "./models/wishlist-item"

export default class WishlistModuleService extends MedusaService({
  Wishlist,
  WishlistItem,
}) {}
```

The `WishlistModuleService` extends `MedusaService` from the Modules SDK which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods.

So, the `WishlistModuleService` class now has methods like `createWishlists` and `retrieveWishlist`.

Find all methods generated by the `MedusaService` in [this reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md).

You'll use this service in a later method to store and manage wishlists and wishlist items in other customizations.

### Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/wishlist/index.ts` with the following content:

![Directory structure after adding the module definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737461829/Medusa%20Resources/wishlist-5_mb4tjf.jpg)

```ts title="src/modules/wishlist/index.ts"
import WishlistModuleService from "./service"
import { Module } from "@medusajs/framework/utils"

export const WISHLIST_MODULE = "wishlist"

export default Module(WISHLIST_MODULE, {
  service: WishlistModuleService,
})
```

You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `wishlist`.
2. An object with a required property `service` indicating the module's service.

You'll later use the module's service to manage wishlists and wishlist items in other customizations.

### Generate Migrations

Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module.

Learn more about migrations in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md).

Medusa's CLI tool generates the migrations for you. To generate a migration for the Wishlist Module, run the following command in the plugin project:

```bash title="Plugin project"
npx medusa plugin:db:generate
```

You'll now have a `migrations` directory under `src/modules/wishlist` that holds the generated migration.

Then, to reflect these migrations on the database of the Medusa application using this module, run the following command:

Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry.

```bash title="Medusa application"
npx medusa db:migrate
```

The tables of the Wishlist Module's data models are now created in the database.

***

## Step 5: Link Wishlist Data Models with Core Models

The Wishlist Module's data models store IDs of records in data models implemented in Medusa's core Commerce Modules, such as the ID of a customer or a product variant.

However, modules are [isolated](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md) to ensure they're re-usable and don't have side effects when integrated into the Medusa application. So, to build associations between modules, you define [module links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md). A Module link associates two modules' data models while maintaining module isolation.

In this section, you'll link the `Wishlist` data model to the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md)'s `Customer` data model, and to the [Sales Channel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md) Module's `SalesChannel` data model. You'll also link the `WishlistItem` data model to the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `ProductVariant` data model.

Learn more about module links in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md).

To create the link between the `Wishlist` data model and the `Customer` data model, create the file `src/modules/wishlist/links/wishlist-customer.ts` with the following content:

![Directory structure after adding the link file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737462649/Medusa%20Resources/wishlist-6_xqytog.jpg)

```ts title="src/modules/wishlist/links/wishlist-customer.ts"
import { defineLink } from "@medusajs/framework/utils"
import WishlistModule from "../modules/wishlist"
import CustomerModule from "@medusajs/medusa/customer"

export default defineLink(
  {
    linkable: WishlistModule.linkable.wishlist.id,
    field: "customer_id",
  },
  CustomerModule.linkable.customer.id,
  {
    readOnly: true,
  }
)
```

You define a link using `defineLink` from the Modules SDK. It accepts three parameters:

1. The first data model part of the link, which is the Wishlist Module's `wishlist` data model. A module has a special `linkable` property that contain link configurations for its data models. You also specify the field that points to the customer.
2. The second data model part of the link, which is the Customer Module's `customer` data model.
3. An object of configurations for the module link. By default, Medusa creates a table in the database to represent the link you define. However, in this guide, you only want this link to retrieve the customer associated with a wishlist. So, you enable `readOnly` telling Medusa not to create a table for this link.

Next, to create the link between the `Wishlist` data model and the `SalesChannel` data model, create the file `src/modules/wishlist/links/wishlist-sales-channel.ts` with the following content:

![Directory structure after adding the link file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737462829/Medusa%20Resources/wishlist-7_ddiwxy.jpg)

```ts title="src/modules/wishlist/links/wishlist-sales-channel.ts"
import { defineLink } from "@medusajs/framework/utils"
import WishlistModule from "../modules/wishlist"
import SalesChannelModule from "@medusajs/medusa/sales-channel"

export default defineLink(
  {
    linkable: WishlistModule.linkable.wishlist.id,
    field: "sales_channel_id",
  },
  SalesChannelModule.linkable.salesChannel,
  {
    readOnly: true,
  }
)
```

You define a link between the `Wishlist` data model and the `SalesChannel` data model in the same way as the previous link.

Finally, to create the link between the `WishlistItem` data model and the `ProductVariant` data model, create the file `src/modules/wishlist/links/wishlist-product.ts` with the following content:

![Directory structure after adding the link file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737467010/Medusa%20Resources/wishlist-8_hgoaby.jpg)

```ts title="src/modules/wishlist/links/wishlist-product.ts"
import { defineLink } from "@medusajs/framework/utils"
import WishlistModule from "../modules/wishlist"
import ProductModule from "@medusajs/medusa/product"

export default defineLink(
  {
    linkable: WishlistModule.linkable.wishlistItem.id,
    field: "product_variant_id",
  },
  ProductModule.linkable.productVariant,
  {
    readOnly: true,
  }
)
```

You define a link between the `WishlistItem` data model and the `ProductVariant` data model in the same way as the previous links.

In the next steps, you'll see how these links allow you to retrieve the resources associated with a wishlist or wishlist item.

***

## Step 6: Create Wishlist Workflow

The first feature you'll add to the wishlist plugin is the ability to create a wishlist for a customer. You'll implement this feature in a workflow.

A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an endpoint.

In this section, you'll create a workflow that creates a wishlist for a customer. Later, you'll execute this workflow from an API route.

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md)

The workflow has the following steps:

- [validateCustomerCreateWishlistStep](#validateCustomerCreateWishlistStep): Validate that the customer doesn't have an existing wishlist.
- [createWishlistStep](#createWishlistStep): Create a wishlist for the customer.

You'll implement the steps before implementing the workflow.

### validateCustomerCreateWishlistStep

The first step in the workflow will validate that a customer doesn't have an existing workflow. If not valid, the step will throw an error, stopping the workflow's execution.

To create the step, create the file `src/workflows/steps/validate-customer-create-wishlist.ts` with the following content:

![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737467678/Medusa%20Resources/wishlist-9_lzlifn.jpg)

```ts title="src/workflows/steps/validate-customer-create-wishlist.ts" highlights={validateCustomerWishlistHighlights}
import { MedusaError } from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk"

type ValidateCustomerCreateWishlistStepInput = {
  customer_id: string
}

export const validateCustomerCreateWishlistStep = createStep(
  "validate-customer-create-wishlist",
  async ({ customer_id }: ValidateCustomerCreateWishlistStepInput, { container }) => {
    const query = container.resolve("query")

    const { data } = await query.graph({
      entity: "wishlist",
      fields: ["*"],
      filters: {
        customer_id: customer_id,
      },
    })

    if (data.length) {
      throw new MedusaError(
        MedusaError.Types.NOT_FOUND,
        "Customer already has a wishlist"
      )
    }

    // check that customer exists
    const { data: customers } = await query.graph({
      entity: "customer",
      fields: ["*"],
      filters: {
        id: customer_id,
      },
    })

    if (customers.length === 0) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Specified customer was not found"
      )
    }
  }
)
```

You create a step using `createStep` from the Workflows SDK. It accepts two parameters:

1. The step's name, which is `validate-customer-create-wishlist`.
2. An async function that executes the step's logic. The function receives two parameters:
   - The input data for the step, which in this case is an object having a `customer_id` property.
   - An object holding the workflow's context, including the [Medusa Container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) that allows you to resolve Framework and commerce tools.

In the step function, you use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve the wishlist based on the specified customer ID. If a wishlist exists, you throw an error, stopping the workflow's execution.

You also try to retrieve the customer, and if they don't exist, you throw an error.

### createWishlistStep

The second step in the workflow will create a wishlist for the customer. To create the step, create the file `src/workflows/steps/create-wishlist.ts` with the following content:

![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737467998/Medusa%20Resources/wishlist-10_xex4d0.jpg)

```ts title="src/workflows/steps/create-wishlist.ts" highlights={createWishlistStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { WISHLIST_MODULE } from "../../modules/wishlist"
import WishlistModuleService from "../../modules/wishlist/service"

type CreateWishlistStepInput = {
  customer_id: string
  sales_channel_id: string
}

export const createWishlistStep = createStep(
  "create-wishlist",
  async (input: CreateWishlistStepInput, { container }) => {
    const wishlistModuleService: WishlistModuleService = 
      container.resolve(WISHLIST_MODULE)

    const wishlist = await wishlistModuleService.createWishlists(input)

    return new StepResponse(wishlist, wishlist.id)
  },
  async (id, { container }) => {
    if (!id) {
      return
    }
    const wishlistModuleService: WishlistModuleService = 
      container.resolve(WISHLIST_MODULE)

    await wishlistModuleService.deleteWishlists(id)
  }
)
```

This step accepts the IDs of the customer and the sales channel as input. In the step function, you resolve the Wishlist Module's service from the container and use its generated `createWishlists` method to create the wishlist, passing it the input as a parameter.

Steps that return data must return them in a `StepResponse` instance. The `StepResponse` constructor accepts two parameters:

- The data to return, which in this case is the created wishlist.
- The data to pass to the compensation function, which in this case is the wishlist's ID.

The compensation function is an optional third parameter of `createStep`. It defines rollback logic that's executed when an error occurs during the workflow's execution. In the compensation function, you undo the actions you performed in the step function.

The compensation function accepts as a first parameter the data passed as a second parameter to the `StepResponse` returned by the step function, which in this case is the wishlist's ID. In the compensation function, you resolve the Wishlist Module's service from the container and use its generated `deleteWishlists` method to delete the wishlist.

Learn more about the generated [create](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/methods/create/index.html.md) and [delete](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/methods/delete/index.html.md) methods.

### Add createWishlistWorkflow

You can now add the `createWishlistWorkflow` to the plugin. Create the file `src/workflows/create-wishlist.ts` with the following content:

![Directory structure after adding the workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737468333/Medusa%20Resources/wishlist-11_absftb.jpg)

```ts title="src/workflows/create-wishlist.ts" highlights={createWishlistWorkflowHighlights}
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { validateCustomerCreateWishlistStep } from "./steps/validate-customer-create-wishlist"
import { createWishlistStep } from "./steps/create-wishlist"

type CreateWishlistWorkflowInput = {
  customer_id: string
  sales_channel_id: string
}

export const createWishlistWorkflow = createWorkflow(
  "create-wishlist",
  (input: CreateWishlistWorkflowInput) => {
    validateCustomerCreateWishlistStep({
      customer_id: input.customer_id,
    })

    const wishlist = createWishlistStep(input)

    return new WorkflowResponse({
      wishlist,
    })
  }
)
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function, which is the workflow's implementation. In the workflow, you:

- Execute the `validateCustomerCreateWishlistStep` step to validate that the customer doesn't have an existing wishlist.
- Execute the `createWishlistStep` step to create the wishlist.

A workflow's constructor function has some constraints in implementation, which is why you need to use `transform` for variable manipulation. Learn more about these constraints in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md).

Workflows must return an instance of `WorkflowResponse`, passing as a parameter the data to return to the workflow's executor. The workflow returns an object having a `wishlist` property, which is the created wishlist.

You'll execute this workflow in an API route in the next step.

***

## Step 7: Create Wishlist API Route

Now that you implemented the flow to create a wishlist for a customer, you'll create an API route that exposes this functionality.

An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts. You'll create a `POST` API route at the path `/store/customers/me/wishlists` that executes the workflow from the previous step.

Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

So, to create the `/store/customers/me/wishlists` API route, create the file `src/api/store/customers/me/wishlists/route.ts` with the following content:

![Directory structure after adding the route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737468859/Medusa%20Resources/wishlist-12_gvvb9z.jpg)

```ts title="src/api/store/customers/me/wishlists/route.ts"
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { createWishlistWorkflow } from "../../../../../workflows/create-wishlist"
import { MedusaError } from "@medusajs/framework/utils"

export async function POST(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  if (!req.publishable_key_context?.sales_channel_ids.length) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "At least one sales channel ID is required to be associated with the publishable API key in the request header."
    )
  }
  const { result } = await createWishlistWorkflow(req.scope)
    .run({
      input: {
        customer_id: req.auth_context.actor_id,
        sales_channel_id: req.publishable_key_context?.sales_channel_ids[0],
      },
    })

  res.json({
    wishlist: result.wishlist,
  })
}
```

Since you export a `POST` function in this file, you're exposing a `POST` API route at `/store/customers/me/wishlists`. The route handler function accepts two parameters:

1. A request object with details and context about the request, such as authenticated customer details.
2. A response object to manipulate and send the response.

API routes implemented under the `/store` path require passing a publishable API key in the header of the request. The publishable API key is created by an admin user and is associated with one or more sales channels. In the route handler function, you validate that the request has at least one sales channel ID associated with the publishable API key. You'll use that sales channel ID with the wishlist you're creating.

Also, API routes implemented under the `/store/customers/me` path are only accessible by authenticated customers. You access the ID of the authenticated customer using the `auth_context.actor_id` property of the request object.

In the route handler function, you execute the `createWishlistWorkflow`, passing the authenticated customer ID and the sales channel ID as input. The workflow returns an object having a `result` property, which is the data returned by the workflow. You return the created wishlist in the response.

### Test API Route

You'll now test that this API route defined in the plugin is working as expected using the Medusa application you installed in the first step.

Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry.

In the Medusa application's directory, run the following command to start the development server:

```bash npm2yarn
npm run dev
```

### Retrieve Publishable API Key

Before sending the request, you need to obtain a publishable API key. So, open the Medusa Admin at `http://localhost:9000/app` and log in with the user you created earlier.

To access your application's API keys in the admin, go to Settings -> Publishable API Keys. You'll have an API key created by default, which is associated with the default sales channel. You can use this publishable API key in the request header.

![In the admin, click on Publishable API key in the sidebar. A table will show your API keys and allow you to create one.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733230421/Medusa%20Resources/Screenshot_2024-12-03_at_2.53.07_PM_gau9jy.png)

### Retrieve Authenticated Customer Token

Then, you need an authentication token of a registered customer. To create a customer, first, send the following request to the Medusa application:

```bash
curl -X POST 'http://localhost:9000/auth/customer/emailpass/register' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "customer@gmail.com",
    "password": "supersecret"
}'
```

This API route obtains a registration token for the specified email and password in the request body.

Next, use that token to register the customer:

```bash
curl -X POST 'http://localhost:9000/store/customers' \
--header 'Content-Type: application/json' \
-H 'x-publishable-api-key: {api_key}' \
--header 'Authorization: Bearer {token}' \
--data-raw '{
    "email": "customer@gmail.com"
}'
```

Make sure to replace `{api_key}` with the publishable API key you copied from the settings, and `{token}` with the token received from the previous request.

This will create a customer. You can now obtain the customer's authentication token by sending the following request:

```bash
curl -X POST 'http://localhost:9000/auth/customer/emailpass' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "customer@gmail.com",
    "password": "supersecret"
}'
```

This API route will return an authentication token for the customer. You'll use this token in the header of the following requests.

### Send Request to Create Wishlist

Finally, send a `POST` request to the `/store/customers/me/wishlists` API route to create a wishlist for the authenticated customer:

```bash
curl -X POST 'localhost:9000/store/customers/me/wishlists' \
--header 'x-publishable-api-key: {api_key}' \
--header 'Authorization: Bearer {token}'
```

Make sure to replace `{api_key}` with the publishable API key you copied from the settings, and `{token}` with the authenticated customer token.

You'll receive in the response the created wishlist.

***

## Step 8: Retrieve Wishlist API Route

In this step, you'll add an API route to retrieve a customer's wishlist. You'll create a `GET` API route at the path `/store/customers/me/wishlists` that retrieves the wishlist of the authenticated customer.

So, add to the `src/api/store/customers/me/wishlists/route.ts` the following:

```ts title="src/api/store/customers/me/wishlists/route.ts"
export async function GET(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  const query = req.scope.resolve("query")

  const { data } = await query.graph({
    entity: "wishlist",
    fields: ["*", "items.*", "items.product_variant.*"],
    filters: {
      customer_id: req.auth_context.actor_id,
    },
  })

  if (!data.length) {
    throw new MedusaError(
      MedusaError.Types.NOT_FOUND,
      "No wishlist found for customer"
    )
  }

  return res.json({
    wishlist: data[0],
  })
}
```

In this route handler function, you use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve the wishlist of the authenticated customer. For each wishlist, you retrieve its items, and the product variants of those items.

If the wishlist doesn't exist, you throw an error. Otherwise, you return the wishlist in the response.

### Test Retrieve Wishlist API Route

To test the API route, start the Medusa application.

Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry.

Then, send a `GET` request to the `/store/customers/me/wishlists` API route:

```bash
curl 'localhost:9000/store/customers/me/wishlists' \
--header 'x-publishable-api-key: {api_key}' \
--header 'Authorization: Bearer {token}'
```

Make sure to replace:

- `{api_key}` with the publishable API key you copied from the settings, as explained in the [previous step](#retrieve-publishable-api-key).
- `{token}` with the authenticated customer token you received from the [previous step](#retrieve-authenticated-customer-token).

You'll receive in the response the wishlist of the authenticated customer.

***

## Step 9: Add Item to Wishlist API Route

Next, you'll add the functionality to add an item to a wishlist. You'll first define a workflow that implements this functionality, then create an API route that executes the workflow.

### Add Item to Wishlist Workflow

The workflow to add an item to a wishlist has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the wishlist of a customer.
- [validateWishlistExistsStep](#validateWishlistExistsStep): Validate that the customer's wishlist exists.
- [validateWishlistSalesChannelStep](#validateWishlistSalesChannelStep): Validate that the wishlist belongs to the specified sales channel.
- [validateVariantWishlistStep](#validateVariantWishlistStep): Validate that the specified variant is not already in the wishlist.
- [createWishlistItemStep](#createWishlistItemStep): Create the wishlist item.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the wishlist again with the new item added.

The `useQueryGraphStep` is from Medusa's workflows package. So, you'll only implement the other steps.

#### validateWishlistExistsStep

The second step in the workflow validates that the customer's wishlist, retrieved in the first step, exists.

To create the step, create the file `src/workflows/steps/validate-wishlist-exists.ts` with the following content:

![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1740071251/Medusa%20Resources/wishlist-29_bq6kcn.jpg)

```ts title="src/workflows/steps/validate-wishlist-exists.ts"
import { MedusaError } from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { InferTypeOf } from "@medusajs/framework/types"
import { Wishlist } from "../../modules/wishlist/models/wishlist"

type Input = {
  wishlists?: InferTypeOf<typeof Wishlist>[]
}

export const validateWishlistExistsStep = createStep(
  "validate-wishlist-exists",
  async (input: Input) => {
    if (!input.wishlists?.length) {
      throw new MedusaError(
        MedusaError.Types.NOT_FOUND,
        "No wishlist found for this customer"
      )
    }
  }
)
```

This step receives an array of wishlists and throws an error if it's empty. You'll use this to stop the workflow's execution if the customer doesn't have a wishlist.

#### validateWishlistSalesChannelStep

The third step in the workflow validates that the wishlist belongs to the sales channel specified in the input.

To create the step, create the file `src/workflows/steps/validate-wishlist-sales-channel.ts` with the following content:

![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737470093/Medusa%20Resources/wishlist-13_nn924e.jpg)

```ts title="src/workflows/steps/validate-wishlist-sales-channel.ts"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { InferTypeOf } from "@medusajs/framework/types"
import { Wishlist } from "../../modules/wishlist/models/wishlist"

type ValidateWishlistSalesChannelStepInput = {
  wishlist: InferTypeOf<typeof Wishlist>
  sales_channel_id: string
}

export const validateWishlistSalesChannelStep = createStep(
  "validate-wishlist-sales-channel",
  async (input: ValidateWishlistSalesChannelStepInput, { container }) => {
    const { wishlist, sales_channel_id } = input

    if (wishlist.sales_channel_id !== sales_channel_id) {
      throw new Error("Wishlist does not belong to the current sales channel")
    }
  }
)
```

This step receives the wishlist object and the sales channel ID as input. In the step function, if the wishlist's sales channel ID doesn't match the sales channel ID in the input, you throw an error.

To represent a data model in a type, use the [InferTypeOf](https://docs.medusajs.com/docs/learn/fundamentals/data-models/infer-type/index.html.md) utility.

#### validateVariantWishlistStep

The next step in the workflow validates that the specified variant is not already in the wishlist.

Create the file `src/workflows/steps/validate-variant-wishlist.ts` with the following content:

![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737470156/Medusa%20Resources/wishlist-14_ckoesz.jpg)

```ts title="src/workflows/steps/validate-variant-wishlist.ts" highlights={validateVariantWishlistHighlights}
import { InferTypeOf } from "@medusajs/framework/types"
import { Wishlist } from "../../modules/wishlist/models/wishlist"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { MedusaError } from "@medusajs/framework/utils"

type ValidateVariantWishlistStepInput = {
  variant_id: string
  sales_channel_id: string
  wishlist: InferTypeOf<typeof Wishlist>
}

export const validateVariantWishlistStep = createStep(
  "validate-variant-in-wishlist",
  async ({ 
    variant_id, 
    sales_channel_id,
    wishlist,
  }: ValidateVariantWishlistStepInput, { container }) => {
    // validate whether variant is in wishlist
    const isInWishlist = wishlist.items?.some(
      (item) => item.product_variant_id === variant_id
    )

    if (isInWishlist) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Variant is already in wishlist"
      )
    }

    // validate that the variant is available in the specified sales channel
    const query = container.resolve("query")
    const { data } = await query.graph({
      entity: "variant",
      fields: ["product.sales_channels.*"],
      filters: {
        id: variant_id,
      },
    })

    const variantInSalesChannel = data[0].product.sales_channels.some(
      (sc) => sc.id === sales_channel_id
    )

    if (!variantInSalesChannel) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Variant is not available in the specified sales channel"
      )
    }
  }
)
```

This step receives the variant ID, sales channel ID, and wishlist object as input. In the step function, you throw an error if:

- The variant is already in the wishlist.
- The variant is not available in the specified sales channel. You use Query to retrieve the sales channels that the variant's product is available in.

#### createWishlistItemStep

The fifth step in the workflow creates a wishlist item for the specified variant in the wishlist.

Create the file `src/workflows/steps/create-wishlist-item.ts` with the following content:

![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737470302/Medusa%20Resources/wishlist-15_oc696x.jpg)

```ts title="src/workflows/steps/create-wishlist-item.ts" highlights={createWishlistItemStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import WishlistModuleService from "../../modules/wishlist/service"
import { WISHLIST_MODULE } from "../../modules/wishlist"

type CreateWishlistItemStepInput = {
  wishlist_id: string
  product_variant_id: string
}

export const createWishlistItemStep = createStep(
  "create-wishlist-item",
  async (input: CreateWishlistItemStepInput, { container }) => {
    const wishlistModuleService: WishlistModuleService = 
      container.resolve(WISHLIST_MODULE)

    const item = await wishlistModuleService.createWishlistItems(input)

    return new StepResponse(item, item.id)
  },
  async (id, { container }) => {
    if (!id) {
      return
    }
    const wishlistModuleService: WishlistModuleService = 
      container.resolve(WISHLIST_MODULE)

    await wishlistModuleService.deleteWishlistItems(id)
  }
)
```

This step receives the wishlist ID and the variant ID as input. In the step function, you resolve the Wishlist Module's service from the container and use its generated `createWishlistItems` method to create the wishlist item, passing it the input as a parameter.

You return the created wishlist item and pass the item's ID to the compensation function. In the compensation function, you resolve the Wishlist Module's service from the container and use its generated `deleteWishlistItems` method to delete the wishlist item if an error occurs in the workflow.

#### Add Item to Wishlist Workflow

You can now add the `createWishlistItemWorkflow` to the plugin. Create the file `src/workflows/create-wishlist-item.ts` with the following content:

![Directory structure after adding the workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737470660/Medusa%20Resources/wishlist-16_ovujwp.jpg)

```ts title="src/workflows/create-wishlist-item.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports" highlights={createWishlistItemWorkflowHighlights}
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { validateWishlistSalesChannelStep } from "./steps/validate-wishlist-sales-channel"
import { createWishlistItemStep } from "./steps/create-wishlist-item"
import { validateVariantWishlistStep } from "./steps/validate-variant-wishlist"
import { validateWishlistExistsStep } from "./steps/validate-wishlist-exists"

type CreateWishlistItemWorkflowInput = {
  variant_id: string
  customer_id: string
  sales_channel_id: string
}

export const createWishlistItemWorkflow = createWorkflow(
  "create-wishlist-item",
  (input: CreateWishlistItemWorkflowInput) => {
    const { data: wishlists } = useQueryGraphStep({
      entity: "wishlist",
      fields: ["*", "items.*"],
      filters: {
        customer_id: input.customer_id,
      },
    })

    validateWishlistExistsStep({
      wishlists,
    })

    validateWishlistSalesChannelStep({
      wishlist: wishlists[0],
      sales_channel_id: input.sales_channel_id,
    })


    validateVariantWishlistStep({
      variant_id: input.variant_id,
      sales_channel_id: input.sales_channel_id,
      wishlist: wishlists[0],
    })

    createWishlistItemStep({
      product_variant_id: input.variant_id,
      wishlist_id: wishlists[0].id,
    })

    // refetch wishlist
    const { data: updatedWishlists } = useQueryGraphStep({
      entity: "wishlist",
      fields: ["*", "items.*", "items.product_variant.*"],
      filters: {
        id: wishlists[0].id,
      },
    }).config({ name: "refetch-wishlist" })

    return new WorkflowResponse({
      wishlist: updatedWishlists[0],
    })
  }
)
```

You create a `createWishlistItemWorkflow`. In the workflow, you:

- Use the [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md) to retrieve the wishlist of a customer. Notice that you pass the link definition between a wishlist and a customer as an entry point to Query. This allows you to filter the wishlist by the customer ID.
- Use the `validateWishlistSalesChannelStep` step to validate that the wishlist belongs to the sales channel specified in the input.
- Use the `validateVariantWishlistStep` step to validate that the variant specified in the input is not already in the wishlist.
- Use the `createWishlistItemStep` step to create the wishlist item.
- Use the `useQueryGraphStep` again to retrieve the wishlist with the new item added.

You return the wishlist with its items.

### Add Item to Wishlist API Route

You'll now create an API route that executes the `createWishlistItemWorkflow` to add an item to a wishlist.

Create the file `src/api/store/customers/me/wishlists/items/route.ts` with the following content:

![Directory structure after adding the route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737470985/Medusa%20Resources/wishlist-17_zmqk6c.jpg)

```ts title="src/api/store/customers/me/wishlists/items/route.ts"
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework"
import { createWishlistItemWorkflow } from "../../../../../../workflows/create-wishlist-item"
import { MedusaError } from "@medusajs/framework/utils"

type PostStoreCreateWishlistItemType = {
  variant_id: string
}

export async function POST(
  req: AuthenticatedMedusaRequest<PostStoreCreateWishlistItemType>,
  res: MedusaResponse
) {
  if (!req.publishable_key_context?.sales_channel_ids.length) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "At least one sales channel ID is required to be associated with the publishable API key in the request header."
    )
  }
  const { result } = await createWishlistItemWorkflow(req.scope)
    .run({
      input: {
        variant_id: req.validatedBody.variant_id,
        customer_id: req.auth_context.actor_id,
        sales_channel_id: req.publishable_key_context?.sales_channel_ids[0],
      },
    })

  res.json({
    wishlist: result.wishlist,
  })
}
```

This route exposes a `POST` endpoint at `/store/customers/me/wishlists/items`. Notice that the `AuthenticatedMedusaRequest` accepts a type parameter indicating the type of the accepted request body. In this case, the request body must have a `variant_id` property, indicating the ID of the variant to add to the wishlist.

In the route handler function, you execute the `createWishlistItemWorkflow` workflow, passing the authenticated customer ID, the variant ID, and the sales channel ID as input. You return in the response the updated wishlist.

### Add Validation Schema

To ensure that a variant ID is passed in the body of requests sent to this API route, you'll define a validation schema for the request body.

In Medusa, you create validation schemas using [Zod](https://zod.dev/) in a TypeScript file under the `src/api` directory. So, create the file `src/api/store/customers/me/wishlists/items/validators.ts` with the following content:

![Directory structure after adding the validation schema file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737471383/Medusa%20Resources/wishlist-18_hj9iom.jpg)

```ts title="src/api/store/customers/me/wishlists/items/validators.ts"
import { z } from "zod"

export const PostStoreCreateWishlistItem = z.object({
  variant_id: z.string(),
})
```

You create an object schema with a `variant_id` property of type `string`.

Learn more about creating schemas in [Zod's documentation](https://zod.dev/).

You can now replace the `PostStoreCreateWishlistItemType` type in `src/api/store/customers/me/wishlists/items/route.ts` with the following:

```ts title="src/api/store/customers/me/wishlists/items/route.ts"
// ...
import { z } from "zod"
import { PostStoreCreateWishlistItem } from "./validators"

type PostStoreCreateWishlistItemType = z.infer<
  typeof PostStoreCreateWishlistItem
>
```

Finally, to use the schema for validation, you need to apply the `validateAndTransformBody` middleware on the `/store/customers/me/wishlists/items` route. A middleware is a function executed before the API route when a request is sent to it.

The `validateAndTransformBody` middleware is available out-of-the-box in Medusa, allowing you to validate and transform the request body using a Zod schema.

Learn more about middlewares in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

To apply the middleware, create the file `src/api/middlewares.ts` with the following content:

![Directory structure after adding the middleware file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737471615/Medusa%20Resources/wishlist-19_ryyzdk.jpg)

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { 
  PostStoreCreateWishlistItem,
} from "./store/customers/me/wishlists/items/validators"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/store/customers/me/wishlists/items",
      method: "POST",
      middlewares: [
        validateAndTransformBody(PostStoreCreateWishlistItem),
      ],
    },
  ],
})
```

In this file, you export the middlewares definition using `defineMiddlewares` from the Medusa Framework. This function accepts an object having a `routes` property, which is an array of middleware configurations to apply on routes.

You pass in the `routes` array an object having the following properties:

- `matcher`: The route to apply the middleware on.
- `method`: The HTTP method to apply the middleware on for the specified API route.
- `middlewares`: An array of the middlewares to apply. You apply the following middleware:
  - `validateAndTransformBody`: A middleware to ensure the received request body is valid against the Zod schema you defined earlier.

Any request sent to `/store/customers/me/wishlists/items` will now automatically fail if its body parameters don't match the `PostStoreCreateWishlistItem` validation schema.

### Test API Route

Start the Medusa application to test out the API route.

Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry.

#### Retrieve Variant ID

To retrieve an ID of a variant to add to the wishlist, send a `GET` request to the `/store/products` API route:

```bash
curl 'localhost:9000/store/products' \
--header 'x-publishable-api-key: {api_key}'
```

Make sure to replace `{api_key}` with the publishable API key you copied from the settings, as explained in [a previous section](#retrieve-publishable-api-key).

The response will contain a list of products. You can use the `id` of a product's variant to add to the wishlist.

#### Add Variant to Wishlist

Then, send a `POST` request to the `/store/customers/me/wishlists/items` API route to add the variant to the wishlist:

```bash
curl -X POST 'localhost:9000/store/customers/me/wishlists/items' \
--header 'Content-Type: application/json'  \
--header 'x-publishable-api-key: {api_key}' \
--header 'Authorization: Bearer {token}' \
--data-raw '{
    "variant_id": "{variant_id}"
}'
```

Make sure to replace:

- `{api_key}` with the publishable API key you copied from the settings, as explained in [a previous section](#retrieve-publishable-api-key).
- `{token}` with the authenticated customer token, as explained in [a previous section](#retrieve-authenticated-customer-token).
- `{variant_id}` with the ID of the variant you retrieved from the `/store/products` API route.

You'll receive in the response the updated wishlist with the added item.

***

## Step 10: Remove Item from Wishlist API Route

In this step, you'll add the functionality to remove an item from a wishlist. You'll first define a workflow that implements this functionality, then create an API route that executes the workflow.

### Remove Item from Wishlist Workflow

The workflow to remove an item from a wishlist has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the wishlist of a customer.
- [validateWishlistExistsStep](#validateWishlistExistsStep): Validate that the customer's wishlist exists.
- [validateItemInWishlistStep](#validateItemInWishlistStep): Validate that the item is in the customer's wishlist.
- [deleteWishlistItemStep](#deleteWishlistItemStep): Delete the wishlist item.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the wishlist again with the item removed.

The `useQueryGraphStep` is from Medusa's workflows package, and you implemented the `validateWishlistExistsStep` [previously](#validatewishlistexistsstep) . So, you'll only implement the other steps.

#### validateItemInWishlistStep

The second step of the workflow validates that the item to remove is in the authenticated customer's wishlist.

To create the step, create the file `src/workflows/steps/validate-item-in-wishlist.ts` with the following content:

![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737474621/Medusa%20Resources/wishlist-20_jcwrtf.jpg)

```ts title="src/workflows/steps/validate-item-in-wishlist.ts"
import { InferTypeOf } from "@medusajs/framework/types"
import { Wishlist } from "../../modules/wishlist/models/wishlist"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { MedusaError } from "@medusajs/framework/utils"

type ValidateItemInWishlistStepInput = {
  wishlist: InferTypeOf<typeof Wishlist>
  wishlist_item_id: string
}

export const validateItemInWishlistStep = createStep(
  "validate-item-in-wishlist",
  async ({ 
    wishlist, 
    wishlist_item_id,
  }: ValidateItemInWishlistStepInput, { container }) => {
    const item = wishlist.items.find((item) => item.id === wishlist_item_id)

    if (!item) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Item does not exist in customer's wishlist"
      )
    }
  }
)
```

This step receives the wishlist object and the wishlist item ID as input. In the step function, you find the item in the wishlist by its ID. If the item doesn't exist, you throw an error.

#### deleteWishlistItemStep

The third step of the workflow deletes the item from the wishlist.

Create the file `src/workflows/steps/delete-wishlist-item.ts` with the following content:

![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737474703/Medusa%20Resources/wishlist-21_e50lrg.jpg)

```ts title="src/workflows/steps/delete-wishlist-item.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import WishlistModuleService from "../../modules/wishlist/service"
import { WISHLIST_MODULE } from "../../modules/wishlist"

type DeleteWishlistItemStepInput = {
  wishlist_item_id: string
}

export const deleteWishlistItemStep = createStep(
  "delete-wishlist-item",
  async ({ wishlist_item_id }: DeleteWishlistItemStepInput, { container }) => {
    const wishlistModuleService: WishlistModuleService = 
      container.resolve(WISHLIST_MODULE)

    await wishlistModuleService.softDeleteWishlistItems(wishlist_item_id)

    return new StepResponse(void 0, wishlist_item_id)
  },
  async (wishlistItemId, { container }) => {
    const wishlistModuleService: WishlistModuleService = 
      container.resolve(WISHLIST_MODULE)

    await wishlistModuleService.restoreWishlistItems([wishlistItemId])
  }
)
```

This step receives the wishlist item ID as input. In the step function, you resolve the Wishlist Module's service from the container and use its generated `softDeleteWishlistItems` method to delete the wishlist item.

You pass the deleted wishlist item ID to the compensation function. In the compensation function, you resolve the Wishlist Module's service from the container and use its generated `restoreWishlistItems` method to restore the wishlist item if an error occurs in the workflow.

Learn more about the [softDelete](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/methods/soft-delete/index.html.md) and [restore](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/methods/restore/index.html.md) generated methods.

#### Remove Item from Wishlist Workflow

You can now add the `deleteWishlistItemWorkflow` to the plugin. Create the file `src/workflows/delete-wishlist-item.ts` with the following content:

![Directory structure after adding the workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737474872/Medusa%20Resources/wishlist-22_wt1g36.jpg)

```ts title="src/workflows/delete-wishlist-item.ts" highlights={deleteWishlistItemWorkflowHighlights}
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { deleteWishlistItemStep } from "./steps/delete-wishlist-item"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { validateItemInWishlistStep } from "./steps/validate-item-in-wishlist"
import { validateWishlistExistsStep } from "./steps/validate-wishlist-exists"

type DeleteWishlistItemWorkflowInput = {
  wishlist_item_id: string
  customer_id: string
}

export const deleteWishlistItemWorkflow = createWorkflow(
  "delete-wishlist-item",
  (input: DeleteWishlistItemWorkflowInput) => {
    const { data: wishlists } = useQueryGraphStep({
      entity: "wishlist",
      fields: ["*", "items.*"],
      filters: {
        customer_id: input.customer_id,
      },
    })
    
    validateWishlistExistsStep({
      wishlists,
    })

    validateItemInWishlistStep({
      wishlist: wishlists[0],
      wishlist_item_id: input.wishlist_item_id,
    })

    deleteWishlistItemStep(input)

    // refetch wishlist
    const { data: updatedWishlists } = useQueryGraphStep({
      entity: "wishlist",
      fields: ["*", "items.*", "items.product_variant.*"],
      filters: {
        id: wishlists[0].id,
      },
    }).config({ name: "refetch-wishlist" })

    return new WorkflowResponse({
      wishlist: updatedWishlists[0],
    })
  }
)
```

You create a `deleteWishlistItemWorkflow`. In the workflow, you:

- Use the [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md) to retrieve the wishlist of a customer. Notice that you pass the link definition between a wishlist and a customer as an entry point to Query. This allows you to filter the wishlist by the customer ID.
- Use the `validateItemInWishlistStep` step to validate that the item to remove is in the customer's wishlist.
- Use the `deleteWishlistItemStep` step to delete the item from the wishlist.
- Use the `useQueryGraphStep` again to retrieve the wishlist with the item removed.

You return the wishlist without the removed item.

### Remove Item from Wishlist API Route

You'll now create an API route that executes the `deleteWishlistItemWorkflow` to remove an item from a wishlist.

Create the file `src/api/store/customers/me/wishlists/items/[id]/route.ts` with the following content:

![Directory structure after adding the route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737475074/Medusa%20Resources/wishlist-23_qatcia.jpg)

```ts title="src/api/store/customers/me/wishlists/items/[id]/route.ts"
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework"
import { deleteWishlistItemWorkflow } from "../../../../../../../workflows/delete-wishlist-item"

export async function DELETE(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  const { result } = await deleteWishlistItemWorkflow(req.scope)
    .run({
      input: {
        wishlist_item_id: req.params.id,
        customer_id: req.auth_context.actor_id,
      },
    })

  res.json({
    wishlist: result.wishlist,
  })
}
```

This route exposes a `DELETE` endpoint at `/store/customers/me/wishlists/items/:id`. The `:id` parameter in the route path represents the ID of the wishlist item to remove.

In the route handler function, you execute the `deleteWishlistItemWorkflow` workflow, passing the authenticated customer ID and the wishlist item ID as input. You return in the response the updated wishlist.

### Test API Route

Start the Medusa application to test out the API route.

Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry.

#### Retrieve Wishlist Item ID

To retrieve an ID of a wishlist item to remove, send a `GET` request to the `/store/customers/me/wishlists` API route:

```bash
curl 'localhost:9000/store/customers/me/wishlists' \
--header 'x-publishable-api-key: {api_key}' \
--header 'Authorization: Bearer {token}'
```

Make sure to replace:

- `{api_key}` with the publishable API key you copied from the settings, as explained in [a previous section](#retrieve-publishable-api-key).
- `{token}` with the authenticated customer token, as explained in [a previous section](#retrieve-authenticated-customer-token).

The response will contain the wishlist of the authenticated customer. You can use the `id` of an item in the wishlist to remove.

#### Remove Item from Wishlist

Then, send a `DELETE` request to the `/store/customers/me/wishlists/items/:id` API route to remove the item from the wishlist:

```bash
curl -X DELETE 'localhost:9000/store/customers/me/wishlists/items/{item_id}' \
--header 'x-publishable-api-key: {api_key}' \
--header 'Authorization: Bearer {token}'
```

Make sure to replace:

- `{api_key}` with the publishable API key you copied from the settings, as explained in [a previous section](#retrieve-publishable-api-key).
- `{token}` with the authenticated customer token, as explained in [a previous section](#retrieve-authenticated-customer-token).
- `{item_id}` with the ID of the item you retrieved from the `/store/customers/me/wishlists` API route.

You'll receive in the response the updated wishlist without the removed item.

***

## Step 11: Share Wishlist API Route

In this step, you'll add the functionality to allow customers to share their wishlist with others. The route will return a token that can be passed to another API route that you'll create in the next step to retrieve the shared wishlist.

To create the token and decode it later, you'll use the [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken) package. So, run the following command in the plugin project to install the package:

```bash npm2yarn badgeLabel="Plugin project" badgeColor="orange"
npm install jsonwebtoken
```

Then, to create the API route, create the file `src/api/store/customers/me/wishlists/share/route.ts` with the following content:

![Directory structure after adding the route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737475331/Medusa%20Resources/wishlist-24_tiwjpr.jpg)

```ts title="src/api/store/customers/me/wishlists/share/route.ts"
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework"
import { MedusaError } from "@medusajs/framework/utils"
import jwt from "jsonwebtoken"

export async function POST(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  if (!req.publishable_key_context?.sales_channel_ids.length) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "At least one sales channel ID is required to be associated with the publishable API key in the request header."
    )
  }

  const query = req.scope.resolve("query")

  const { data } = await query.graph({
    entity: "wishlist",
    fields: ["*"],
    filters: {
      customer_id: req.auth_context.actor_id,
    },
  })

  if (!data.length) {
    throw new MedusaError(
      MedusaError.Types.NOT_FOUND,
      "No wishlist found for customer"
    )
  }

  if (data[0].sales_channel_id !== req.publishable_key_context.sales_channel_ids[0]) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "Wishlist does not belong to the specified sales channel"
    )
  }

  // TODO generate the token
}
```

This route exposes a `POST` endpoint at `/store/customers/me/wishlists/share`. In the route handler function, you use Query to retrieve the wishlist of the authenticated customer. If the customer doesn't have a wishlist, or the wishlist doesn't belong to the sales channel specified in the request's publishable API key, you throw an error.

You'll now generate a token that contains the wishlist ID. To do this, replace the `TODO` in the route handler function with the following:

```ts title="src/api/store/customers/me/wishlists/share/route.ts"
const { http } = req.scope.resolve("configModule").projectConfig

const wishlistToken = jwt.sign({
  wishlist_id: data[0].id,
}, http.jwtSecret!, {
  expiresIn: http.jwtExpiresIn,
})

return res.json({
  token: wishlistToken,
})
```

You first retrieve the [http Medusa configuration](https://docs.medusajs.com/docs/learn/configurations/medusa-config#http/index.html.md) which holds configurations related to JWT secrets and expiration times. You then use the `jsonwebtoken` package to sign a token containing the wishlist ID. You return the token in the response.

### Test API Route

Start the Medusa application to test out the API route.

Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry.

Then, send a `POST` request to the `/store/customers/me/wishlists/share` API route to generate a share token for the authenticated customer's wishlist:

```bash
curl -X POST 'localhost:9000/store/customers/me/wishlists/share' \
--header 'x-publishable-api-key: {api_key}' \
--header 'Authorization: Bearer {token}'
```

Make sure to replace:

- `{api_key}` with the publishable API key you copied from the settings, as explained in [a previous section](#retrieve-publishable-api-key).
- `{token}` with the authenticated customer token, as explained in [a previous section](#retrieve-authenticated-customer-token).

You'll receive in the response a token that you can pass to the next API route to retrieve the shared wishlist.

***

## Step 12: Retrieve Shared Wishlist API Route

In this step, you'll add an API route that retrieves a wishlist shared using a token returned by the `/store/customers/me/wishlists/share` API route.

Create the file `src/api/store/wishlists/[token]/route.ts` with the following content:

![Directory structure after adding the route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737475795/Medusa%20Resources/wishlist-25_sodzsr.jpg)

```ts title="src/api/store/wishlists/[token]/route.ts"
import { MedusaResponse, MedusaStoreRequest } from "@medusajs/framework"
import { MedusaError } from "@medusajs/framework/utils"
import { decode, JwtPayload } from "jsonwebtoken"

export async function GET(
  req: MedusaStoreRequest,
  res: MedusaResponse
) {
  if (!req.publishable_key_context?.sales_channel_ids.length) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "At least one sales channel ID is required to be associated with the publishable API key in the request header."
    )
  }
  
  const decodedToken = decode(req.params.token) as JwtPayload

  if (!decodedToken.wishlist_id) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "Invalid token"
    )
  }

  const query = req.scope.resolve("query")

  const { data } = await query.graph({
    entity: "wishlist",
    fields: ["*", "items.*", "items.product_variant.*"],
    filters: {
      id: decodedToken.wishlist_id,
    },
  })

  if (!data.length) {
    throw new MedusaError(
      MedusaError.Types.NOT_FOUND,
      "No wishlist found"
    )
  }

  if (data[0].sales_channel_id !== req.publishable_key_context.sales_channel_ids[0]) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "Wishlist does not belong to the request's sales channel"
    )
  }

  res.json({
    wishlist: data[0],
  })
}
```

This route exposes a `GET` endpoint at `/store/wishlists/:token`. The `:token` parameter in the route path represents the token generated by the `/store/customers/me/wishlists/share` API route.

In the route handler function, you decode the token to retrieve the wishlist ID. If the token is invalid, you throw an error.

Then, you use Query to retrieve the wishlist with the ID from the decoded token. If no wishlist is found or the wishlist doesn't belong to the sales channel ID of the current request, you throw an error.

You return in the response the shared wishlist.

### Test API Route

Start the Medusa application to test out the API route.

Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry.

Then, send a `GET` request to the `/store/wishlists/:token` API route to retrieve the shared wishlist:

```bash
curl 'localhost:9000/store/wishlists/{wishlist_token}' \
--header 'x-publishable-api-key: {api_key}'
```

Make sure to replace:

- `{wishlist_token}` with the token you received from the `/store/customers/me/wishlists/share` API route.
- `{api_key}` with the publishable API key you copied from the settings, as explained in [a previous section](#retrieve-publishable-api-key).

You'll receive in the response the shared wishlist.

***

## Step 13: Show Wishlist Count in Medusa Admin

In this step, you'll customize the Medusa Admin dashboard to show for each product the number of wishlists it's in.

The Medusa Admin dashboard's pages are customizable to insert widgets of custom content in pre-defined injection zones. You create these widgets as React components that allow admin users to perform custom actions.

Learn more about widgets in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md).

### Add Method to Retrieve Wishlist Count

To retrieve the number of wishlists a product is in, you'll add a method to the `WishlistModuleService` that runs a query to retrieve distinct wishlist IDs containing a product variant.

In `src/modules/wishlist/service.ts`, add the following imports and method:

```ts title="src/modules/wishlist/service.ts"
// other imports...
import { InjectManager } from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@medusajs/framework/mikro-orm/knex"

export default class WishlistModuleService extends MedusaService({
  Wishlist,
  WishlistItem,
}) {
  @InjectManager()
  async getWishlistsOfVariants(
    variantIds: string[],
    @MedusaContext() context: Context<EntityManager> = {}
  ): Promise<number> {
    return (await context.manager?.createQueryBuilder("wishlist_item", "wi")
      .select(["wi.wishlist_id"], true)
      .where("wi.product_variant_id IN (?)", [variantIds])
      .execute())?.length || 0
  }
}
```

To perform queries on the database in a method, add the `@InjectManager` decorator to the method. This will inject a [forked MikroORM entity manager](https://mikro-orm.io/docs/identity-map#forking-entity-manager) that you can use in your method.

Methods with the `@InjectManager` decorator accept as a last parameter a context object that has the `@MedusaContext` decorator. The entity manager is injected into the `manager` property of this parameter.

The method accepts an array of variant IDs as a parameter. In the method, you use the `createQueryBuilder` to construct a query, passing it the name of the `WishlistItem`'s table. You then select distinct `wishlist_id`s where the `product_variant_id` of the wishlist item is in the array of variant IDs.

You execute the query and return the number of distinct wishlist IDs containing the product variants. You'll use this method next.

### Create Wishlist Count API Route

Before creating the widget, you'll create the API route that retrieves the number of wishlists a product is in.

Create the file `src/api/store/products/[id]/wishlist/route.ts` with the following content:

![Directory structure after adding the route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737630927/Medusa%20Resources/wishlist-26_ervnfg.jpg)

```ts title="src/api/store/products/[id]/wishlist/route.ts" highlights={wishlistCountRouteHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
import WishlistModuleService from "../../../../../modules/wishlist/service"
import { WISHLIST_MODULE } from "../../../../../modules/wishlist"
import { MedusaError } from "@medusajs/framework/utils"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { id } = req.params

  const query = req.scope.resolve("query")
  const wishlistModuleService: WishlistModuleService = req.scope.resolve(
    WISHLIST_MODULE
  )

  const { data: [product] } = await query.graph({
    entity: "product",
    fields: ["variants.*"],
    filters: {
      id,
    },
  })

  if (!product) {
    throw new MedusaError(
      MedusaError.Types.NOT_FOUND,
      `Product with id: ${id} was not found`
    )
  }

  const count = await wishlistModuleService.getWishlistsOfVariants(
    product.variants.map((v) => v.id)
  )

  res.json({
    count,
  })
}
```

This route exposes a `GET` endpoint at `/store/products/:id/wishlist`. The `:id` parameter in the route path represents the ID of the product to retrieve the wishlist count for.

In the route handler function, you use Query to retrieve the product and its variants, and throw an error if the product doesn't exist.

Then, you resolve the `WishlistModuleService` from the Medusa Container and use its `getWishlistsOfVariants` method to retrieve the number of wishlists the product's variants are in. You return the count in the response.

You'll use this API route in the widget next.

### Create Wishlist Count Widget

You'll now create the widget that will be shown on a product's page in the Medusa Admin.

In the widget, you'll send a request to the API route you created to retrieve the wishlist count for the product. To send the request, you'll use the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), which is a JavaScript library that simplifies sending requests to Medusa's API routes.

To initialize the JS SDK, create the file `src/admin/lib/sdk.ts` with the following content:

![Directory structure after adding the SDK file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737631853/Medusa%20Resources/wishlist-27_pkzeaj.jpg)

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

You initialize an instance of the JS SDK, which you'll use in the widget to send requests.

Learn more about the JS SDK and configuring it in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md).

Then, to create the widget, create the file `src/admin/widgets/product-widget.tsx` with the following content:

![Directory structure after adding the widget file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737631988/Medusa%20Resources/wishlist-28_fx8rw7.jpg)

```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading, Text } from "@medusajs/ui"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"
import { 
  DetailWidgetProps, 
  AdminProduct,
} from "@medusajs/framework/types"

type WishlistResponse = {
  count: number
}

const ProductWidget = ({ 
  data: product,
}: DetailWidgetProps<AdminProduct>) => {
  const { data, isLoading } = useQuery<WishlistResponse>({
    queryFn: () => sdk.client.fetch(`/admin/products/${product.id}/wishlist`),
    queryKey: [["products", product.id, "wishlist"]],
  })

  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">Wishlist</Heading>
      </div>
      <Text className="px-6 py-4">
        {isLoading ? 
          "Loading..." : `This product is in ${data?.count} wishlist(s).`
        }
      </Text>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

A widget file must export a React component and a `config` object created with `defineWidgetConfig` from the Admin Extension SDK. In the `config` object, you specify the zone to inject the widget into in the `zone` property. This widget is injected into a product's page before any other sections.

Find all widget injection zones in [this reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-widget-injection-zones/index.html.md).

Since the widget is injected into a product's details page, it receives the product's details as a `data` prop. In the widget, you use [Tanstack Query](https://tanstack.com/query/latest) to benefit from features like data caching and invalidation. You use the `useQuery` hook to send a request to the API route you created to retrieve the wishlist count for the product.

Finally, you display the widget's content using components from [Medusa UI](https://docs.medusajs.com/ui/index.html.md), allowing you to align the design of your widget with the Medusa Admin's design system.

### Test it Out

To test it out, start the Medusa application.

Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry.

Then:

1. open the Medusa Admin at `localhost:9000/app` and log in.

2. Click on Products in the sidebar, then choose a product from the table.

![Click on the "Products" in the sidebar on the right, then choose a product from the table shown in the middle](https://res.cloudinary.com/dza7lstvk/image/upload/v1737632826/Medusa%20Resources/Screenshot_2025-01-23_at_1.46.29_PM_xjsn8s.png)

3. You should see the widget you created showing the number of wishlists the product is in at the top of the page.

![The widget is shown at the top of the product page before other sections](https://res.cloudinary.com/dza7lstvk/image/upload/v1737632826/Medusa%20Resources/Screenshot_2025-01-23_at_1.46.05_PM_hfyz7u.png)

***

## Next Steps

You've now implemented the wishlist functionality in a Medusa plugin. You can publish that plugin as explained in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/plugins/create#5-publish-plugin-to-npm/index.html.md) to NPM and install it in any Medusa application. This will allow you to re-use your plugin or share it with the community.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


## JS SDK Admin

- [batchSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.batchSalesChannels/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.retrieve/index.html.md)
- [revoke](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.revoke/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.update/index.html.md)
- [batchPromotions](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.batchPromotions/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.update/index.html.md)
- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundItems/index.html.md)
- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundShipping/index.html.md)
- [addItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addItems/index.html.md)
- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundItems/index.html.md)
- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundShipping/index.html.md)
- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancel/index.html.md)
- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancelRequest/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.create/index.html.md)
- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteInboundShipping/index.html.md)
- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteOutboundShipping/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.list/index.html.md)
- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeInboundItem/index.html.md)
- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeItem/index.html.md)
- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeOutboundItem/index.html.md)
- [request](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.request/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.retrieve/index.html.md)
- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundItem/index.html.md)
- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundShipping/index.html.md)
- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateItem/index.html.md)
- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundItem/index.html.md)
- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundShipping/index.html.md)
- [clearToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken/index.html.md)
- [clearToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken_/index.html.md)
- [fetch](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetch/index.html.md)
- [fetchStream](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetchStream/index.html.md)
- [getApiKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getApiKeyHeader_/index.html.md)
- [getJwtHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getJwtHeader_/index.html.md)
- [getPublishableKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getPublishableKeyHeader_/index.html.md)
- [getToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getToken/index.html.md)
- [getTokenStorageInfo\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getTokenStorageInfo_/index.html.md)
- [getToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getToken_/index.html.md)
- [initClient](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.initClient/index.html.md)
- [setLocale](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setLocale/index.html.md)
- [setToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken/index.html.md)
- [setToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken_/index.html.md)
- [throwError\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.throwError_/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.retrieve/index.html.md)
- [getItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.getItem/index.html.md)
- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.removeItem/index.html.md)
- [setItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.setItem/index.html.md)
- [batchCustomerGroups](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.batchCustomerGroups/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.create/index.html.md)
- [createAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.createAddress/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.delete/index.html.md)
- [deleteAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.deleteAddress/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.list/index.html.md)
- [listAddresses](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.listAddresses/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieve/index.html.md)
- [retrieveAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieveAddress/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.update/index.html.md)
- [updateAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.updateAddress/index.html.md)
- [batchCustomers](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.batchCustomers/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.update/index.html.md)
- [addItems](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addItems/index.html.md)
- [addPromotions](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addPromotions/index.html.md)
- [addShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addShippingMethod/index.html.md)
- [beginEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.beginEdit/index.html.md)
- [cancelEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.cancelEdit/index.html.md)
- [confirmEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.confirmEdit/index.html.md)
- [convertToOrder](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.convertToOrder/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.list/index.html.md)
- [removeActionItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeActionItem/index.html.md)
- [removeActionShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeActionShippingMethod/index.html.md)
- [removePromotions](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removePromotions/index.html.md)
- [removeShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeShippingMethod/index.html.md)
- [requestEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.requestEdit/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.update/index.html.md)
- [updateActionItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateActionItem/index.html.md)
- [updateActionShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateActionShippingMethod/index.html.md)
- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateItem/index.html.md)
- [updateShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateShippingMethod/index.html.md)
- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundItems/index.html.md)
- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundShipping/index.html.md)
- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundItems/index.html.md)
- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundShipping/index.html.md)
- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancel/index.html.md)
- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancelRequest/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.create/index.html.md)
- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteInboundShipping/index.html.md)
- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteOutboundShipping/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.list/index.html.md)
- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeInboundItem/index.html.md)
- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeOutboundItem/index.html.md)
- [request](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.request/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.retrieve/index.html.md)
- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundItem/index.html.md)
- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundShipping/index.html.md)
- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundItem/index.html.md)
- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundShipping/index.html.md)
- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.cancel/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.create/index.html.md)
- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.createShipment/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.list/index.html.md)
- [listFulfillmentOptions](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.listFulfillmentOptions/index.html.md)
- [createServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.createServiceZone/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.delete/index.html.md)
- [deleteServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.deleteServiceZone/index.html.md)
- [retrieveServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.retrieveServiceZone/index.html.md)
- [updateServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.updateServiceZone/index.html.md)
- [batchInventoryItemLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemLocationLevels/index.html.md)
- [batchInventoryItemsLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemsLocationLevels/index.html.md)
- [batchUpdateLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchUpdateLevels/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.delete/index.html.md)
- [deleteLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.deleteLevel/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.list/index.html.md)
- [listLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.listLevels/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.update/index.html.md)
- [updateLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.updateLevel/index.html.md)
- [accept](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.accept/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.list/index.html.md)
- [resend](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.resend/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.retrieve/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Locale/methods/js_sdk.admin.Locale.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Locale/methods/js_sdk.admin.Locale.retrieve/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.retrieve/index.html.md)
- [archive](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.archive/index.html.md)
- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancel/index.html.md)
- [cancelFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelFulfillment/index.html.md)
- [cancelTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelTransfer/index.html.md)
- [complete](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.complete/index.html.md)
- [createCreditLine](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createCreditLine/index.html.md)
- [createFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createFulfillment/index.html.md)
- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createShipment/index.html.md)
- [export](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.export/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.list/index.html.md)
- [listChanges](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listChanges/index.html.md)
- [listLineItems](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listLineItems/index.html.md)
- [listShippingOptions](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listShippingOptions/index.html.md)
- [markAsDelivered](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.markAsDelivered/index.html.md)
- [requestTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.requestTransfer/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrieve/index.html.md)
- [retrievePreview](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrievePreview/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.update/index.html.md)
- [updateOrderChange](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.updateOrderChange/index.html.md)
- [addItems](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.addItems/index.html.md)
- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.cancelRequest/index.html.md)
- [confirm](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.confirm/index.html.md)
- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.initiateRequest/index.html.md)
- [removeAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.removeAddedItem/index.html.md)
- [request](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.request/index.html.md)
- [updateAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateAddedItem/index.html.md)
- [updateOriginalItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateOriginalItem/index.html.md)
- [capture](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.capture/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.list/index.html.md)
- [listPaymentProviders](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.listPaymentProviders/index.html.md)
- [refund](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.refund/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.retrieve/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.delete/index.html.md)
- [markAsPaid](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.markAsPaid/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Plugin/methods/js_sdk.admin.Plugin.list/index.html.md)
- [batchPrices](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.batchPrices/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.delete/index.html.md)
- [linkProducts](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.linkProducts/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.list/index.html.md)
- [prices](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.prices/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.update/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.update/index.html.md)
- [batch](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batch/index.html.md)
- [batchImageVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchImageVariants/index.html.md)
- [batchVariantImages](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariantImages/index.html.md)
- [batchVariantInventoryItems](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariantInventoryItems/index.html.md)
- [batchVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariants/index.html.md)
- [confirmImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.confirmImport/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.create/index.html.md)
- [createImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createImport/index.html.md)
- [createOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createOption/index.html.md)
- [createVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createVariant/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.delete/index.html.md)
- [deleteOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteOption/index.html.md)
- [deleteVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteVariant/index.html.md)
- [export](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.export/index.html.md)
- [import](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.import/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.list/index.html.md)
- [listOptions](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listOptions/index.html.md)
- [listVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listVariants/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieve/index.html.md)
- [retrieveOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveOption/index.html.md)
- [retrieveVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveVariant/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.update/index.html.md)
- [updateOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateOption/index.html.md)
- [updateVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateVariant/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.update/index.html.md)
- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.updateProducts/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.update/index.html.md)
- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.updateProducts/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.update/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.update/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductVariant/methods/js_sdk.admin.ProductVariant.list/index.html.md)
- [addRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.addRules/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.list/index.html.md)
- [listRuleAttributes](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleAttributes/index.html.md)
- [listRuleValues](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleValues/index.html.md)
- [listRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRules/index.html.md)
- [removeRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.removeRules/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.update/index.html.md)
- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.updateRules/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/RefundReason/methods/js_sdk.admin.RefundReason.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/RefundReason/methods/js_sdk.admin.RefundReason.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/RefundReason/methods/js_sdk.admin.RefundReason.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/RefundReason/methods/js_sdk.admin.RefundReason.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/RefundReason/methods/js_sdk.admin.RefundReason.update/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.update/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.update/index.html.md)
- [addReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnItem/index.html.md)
- [addReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnShipping/index.html.md)
- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancel/index.html.md)
- [cancelReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelReceive/index.html.md)
- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelRequest/index.html.md)
- [confirmReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmReceive/index.html.md)
- [confirmRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmRequest/index.html.md)
- [deleteReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.deleteReturnShipping/index.html.md)
- [dismissItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.dismissItems/index.html.md)
- [initiateReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateReceive/index.html.md)
- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateRequest/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.list/index.html.md)
- [receiveItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.receiveItems/index.html.md)
- [removeDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeDismissItem/index.html.md)
- [removeReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReceiveItem/index.html.md)
- [removeReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReturnItem/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.retrieve/index.html.md)
- [updateDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateDismissItem/index.html.md)
- [updateReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReceiveItem/index.html.md)
- [updateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateRequest/index.html.md)
- [updateReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnItem/index.html.md)
- [updateReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnShipping/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.update/index.html.md)
- [batchProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.batchProducts/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.update/index.html.md)
- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.updateProducts/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.update/index.html.md)
- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.updateRules/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingOptionType/methods/js_sdk.admin.ShippingOptionType.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingOptionType/methods/js_sdk.admin.ShippingOptionType.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingOptionType/methods/js_sdk.admin.ShippingOptionType.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingOptionType/methods/js_sdk.admin.ShippingOptionType.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingOptionType/methods/js_sdk.admin.ShippingOptionType.update/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.update/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.create/index.html.md)
- [createFulfillmentSet](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.createFulfillmentSet/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.update/index.html.md)
- [updateFulfillmentProviders](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateFulfillmentProviders/index.html.md)
- [updateSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateSalesChannels/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.update/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxProvider/methods/js_sdk.admin.TaxProvider.list/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.update/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.update/index.html.md)
- [batch](https://docs.medusajs.com/references/js_sdk/admin/Translation/methods/js_sdk.admin.Translation.batch/index.html.md)
- [entities](https://docs.medusajs.com/references/js_sdk/admin/Translation/methods/js_sdk.admin.Translation.entities/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Translation/methods/js_sdk.admin.Translation.list/index.html.md)
- [settings](https://docs.medusajs.com/references/js_sdk/admin/Translation/methods/js_sdk.admin.Translation.settings/index.html.md)
- [statistics](https://docs.medusajs.com/references/js_sdk/admin/Translation/methods/js_sdk.admin.Translation.statistics/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.delete/index.html.md)
- [presignedUrl](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.presignedUrl/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.retrieve/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.list/index.html.md)
- [me](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.me/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.update/index.html.md)
- [columns](https://docs.medusajs.com/references/js_sdk/admin/Views/methods/js_sdk.admin.Views.columns/index.html.md)
- [createConfiguration](https://docs.medusajs.com/references/js_sdk/admin/Views/methods/js_sdk.admin.Views.createConfiguration/index.html.md)
- [deleteConfiguration](https://docs.medusajs.com/references/js_sdk/admin/Views/methods/js_sdk.admin.Views.deleteConfiguration/index.html.md)
- [listConfigurations](https://docs.medusajs.com/references/js_sdk/admin/Views/methods/js_sdk.admin.Views.listConfigurations/index.html.md)
- [retrieveActiveConfiguration](https://docs.medusajs.com/references/js_sdk/admin/Views/methods/js_sdk.admin.Views.retrieveActiveConfiguration/index.html.md)
- [retrieveConfiguration](https://docs.medusajs.com/references/js_sdk/admin/Views/methods/js_sdk.admin.Views.retrieveConfiguration/index.html.md)
- [setActiveConfiguration](https://docs.medusajs.com/references/js_sdk/admin/Views/methods/js_sdk.admin.Views.setActiveConfiguration/index.html.md)
- [updateConfiguration](https://docs.medusajs.com/references/js_sdk/admin/Views/methods/js_sdk.admin.Views.updateConfiguration/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.retrieve/index.html.md)


## JS SDK Auth

- [callback](https://docs.medusajs.com/references/js-sdk/auth/callback/index.html.md)
- [login](https://docs.medusajs.com/references/js-sdk/auth/login/index.html.md)
- [logout](https://docs.medusajs.com/references/js-sdk/auth/logout/index.html.md)
- [refresh](https://docs.medusajs.com/references/js-sdk/auth/refresh/index.html.md)
- [register](https://docs.medusajs.com/references/js-sdk/auth/register/index.html.md)
- [resetPassword](https://docs.medusajs.com/references/js-sdk/auth/resetPassword/index.html.md)
- [updateProvider](https://docs.medusajs.com/references/js-sdk/auth/updateProvider/index.html.md)


## JS SDK Store

- [cart](https://docs.medusajs.com/references/js-sdk/store/cart/index.html.md)
- [category](https://docs.medusajs.com/references/js-sdk/store/category/index.html.md)
- [collection](https://docs.medusajs.com/references/js-sdk/store/collection/index.html.md)
- [customer](https://docs.medusajs.com/references/js-sdk/store/customer/index.html.md)
- [fulfillment](https://docs.medusajs.com/references/js-sdk/store/fulfillment/index.html.md)
- [locale](https://docs.medusajs.com/references/js-sdk/store/locale/index.html.md)
- [order](https://docs.medusajs.com/references/js-sdk/store/order/index.html.md)
- [payment](https://docs.medusajs.com/references/js-sdk/store/payment/index.html.md)
- [product](https://docs.medusajs.com/references/js-sdk/store/product/index.html.md)
- [region](https://docs.medusajs.com/references/js-sdk/store/region/index.html.md)


# Action Menu - Admin Components

In this guide, you'll learn how to add an action menu to your Medusa Admin customizations.

The Medusa Admin often provides additional actions in a dropdown shown when users click a three-dot icon.

![Example of an action menu in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728291319/Medusa%20Resources/action-menu_jnus6k.png)

To create a component that shows this menu in your customizations, create the file `src/admin/components/action-menu.tsx` with the following content:

```tsx title="src/admin/components/action-menu.tsx"
import { 
  DropdownMenu,
  IconButton,
  clx,
} from "@medusajs/ui"
import { EllipsisHorizontal } from "@medusajs/icons"
import { Link } from "react-router-dom"

export type Action = {
  icon: React.ReactNode
  label: string
  disabled?: boolean
} & (
  | {
      to: string
      onClick?: never
    }
  | {
      onClick: () => void
      to?: never
    }
)

export type ActionGroup = {
  actions: Action[]
}

export type ActionMenuProps = {
  groups: ActionGroup[]
}

export const ActionMenu = ({ groups }: ActionMenuProps) => {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger asChild>
        <IconButton size="small" variant="transparent">
          <EllipsisHorizontal />
        </IconButton>
      </DropdownMenu.Trigger>
      <DropdownMenu.Content>
        {groups.map((group, index) => {
          if (!group.actions.length) {
            return null
          }

          const isLast = index === groups.length - 1

          return (
            <DropdownMenu.Group key={index}>
              {group.actions.map((action, index) => {
                if (action.onClick) {
                  return (
                    <DropdownMenu.Item
                      disabled={action.disabled}
                      key={index}
                      onClick={(e) => {
                        e.stopPropagation()
                        action.onClick()
                      }}
                      className={clx(
                        "[&_svg]:text-ui-fg-subtle flex items-center gap-x-2",
                        {
                          "[&_svg]:text-ui-fg-disabled": action.disabled,
                        }
                      )}
                    >
                      {action.icon}
                      <span>{action.label}</span>
                    </DropdownMenu.Item>
                  )
                }

                return (
                  <div key={index}>
                    <DropdownMenu.Item
                      className={clx(
                        "[&_svg]:text-ui-fg-subtle flex items-center gap-x-2",
                        {
                          "[&_svg]:text-ui-fg-disabled": action.disabled,
                        }
                      )}
                      asChild
                      disabled={action.disabled}
                    >
                      <Link to={action.to} onClick={(e) => e.stopPropagation()}>
                        {action.icon}
                        <span>{action.label}</span>
                      </Link>
                    </DropdownMenu.Item>
                  </div>
                )
              })}
              {!isLast && <DropdownMenu.Separator />}
            </DropdownMenu.Group>
          )
        })}
      </DropdownMenu.Content>
    </DropdownMenu>
  )
}
```

The `ActionMenu` component shows a three-dots icon (or `EllipsisHorizontal`) from the [Medusa Icons package](https://docs.medusajs.com/ui/icons/overview/index.html.md) in a button.

When the button is clicked, a dropdown menu is shown with the actions passed in the props.

The component accepts the following props:

- groups: (\`object\[]\`) Groups of actions to be shown in the dropdown. Each group is separated by a divider.

  - actions: (\`object\[]\`) Actions in the group.

    - icon: (\`React.ReactNode\`) The icon of the action. You can use icons from the \[Medusa Icons package]\(https://docs.medusajs.com/ui/icons/overview).

    - label: (\`string\`) The action's text.

    - disabled: (\`boolean\`) Whether the action is shown as disabled.

    - \`to\`: (\`string\`) The link to take the user to when they click the action. This is required if \`onClick\` isn't provided.

    - \`onClick\`: (\`() => void\`) The function to execute when the action is clicked. This is required if \`to\` isn't provided.

***

## Example

Use the `ActionMenu` component in any widget or UI route.

For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:

```tsx title="src/admin/widgets/product-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Pencil } from "@medusajs/icons"
import { Container } from "../components/container"
import { ActionMenu } from "../components/action-menu"

const ProductWidget = () => {
  return (
    <Container>
      <ActionMenu groups={[
        {
          actions: [
            {
              icon: <Pencil />,
              label: "Edit",
              onClick: () => {
                alert("You clicked the edit action!")
              },
            },
          ],
        },
      ]} />
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

This widget also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) custom component.

### Use in Header

You can also use the action menu in the [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) component as part of its actions.

For example:

```tsx title="src/admin/widgets/product-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Pencil } from "@medusajs/icons"
import { Container } from "../components/container"
import { Header } from "../components/header"

const ProductWidget = () => {
  return (
    <Container>
      <Header 
        title="Product Widget"
        subtitle="This is my custom product widget"
        actions={[
          {
            type: "action-menu",
            props: {
              groups: [
                {
                  actions: [
                    {
                      icon: <Pencil />,
                      label: "Edit",
                      onClick: () => {
                        alert("You clicked the edit action!")
                      },
                    },
                  ],
                },
              ],
            },
          },
        ]}
      />
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```


# Container - Admin Components

In this guide, you'll learn how to create a container component that matches the Medusa Admin's design conventions.

The Medusa Admin wraps each section of a page in a container.

![Example of a container in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728287102/Medusa%20Resources/container_soenir.png)

To create a component that uses the same container styling in your widgets or UI routes, create the file `src/admin/components/container.tsx` with the following content:

```tsx
import { 
  Container as UiContainer,
  clx,
} from "@medusajs/ui"

type ContainerProps = React.ComponentProps<typeof UiContainer>

export const Container = (props: ContainerProps) => {
  return (
    <UiContainer {...props} className={clx(
      "divide-y p-0",
      props.className
    )} />
  )
}
```

The `Container` component re-uses the component from the [Medusa UI package](https://docs.medusajs.com/ui/components/container/index.html.md) and applies to it classes to match the Medusa Admin's design conventions.

***

## Example

Use that `Container` component in any widget or UI route.

For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:

```tsx title="src/admin/widgets/product-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container } from "../components/container"
import { Header } from "../components/header"

const ProductWidget = () => {
  return (
    <Container>
      <Header title="Product Widget" />
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

This widget also uses a [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component.


# Data Table - Admin Components

This component is available after [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0).

The [DataTable component in Medusa UI](https://docs.medusajs.com/ui/components/data-table/index.html.md) allows you to display data in a table with sorting, filtering, and pagination. It's used across the Medusa Admin dashboard to showcase a list of items, such as a list of products.

![Example of a table in the product listing page](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295658/Medusa%20Resources/list_ddt9zc.png)

You can use this component in your Admin Extensions to display data in a table format, especially if you're retrieving them from API routes of the Medusa application.

This guide focuses on how to use the `DataTable` component while fetching data from the backend. Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/components/data-table/index.html.md) for detailed information about the DataTable component and its different usages.

## Example: DataTable with Data Fetching

In this example, you'll create a UI widget that shows the list of products retrieved from the [List Products API Route](https://docs.medusajs.com/api/admin#products_getproducts) in a data table with pagination, filtering, searching, and sorting.

Start by initializing the columns in the data table. To do that, use the `createDataTableColumnHelper` from Medusa UI:

```tsx title="src/admin/routes/custom/page.tsx"
import {
  createDataTableColumnHelper,
} from "@medusajs/ui"
import { 
  HttpTypes,
} from "@medusajs/framework/types"

const columnHelper = createDataTableColumnHelper<HttpTypes.AdminProduct>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    // Enables sorting for the column.
    enableSorting: true,
    // If omitted, the header will be used instead if it's a string, 
    // otherwise the accessor key (id) will be used.
    sortLabel: "Title",
    // If omitted the default value will be "A-Z"
    sortAscLabel: "A-Z",
    // If omitted the default value will be "Z-A"
    sortDescLabel: "Z-A",
  }),
  columnHelper.accessor("status", {
    header: "Status",
    cell: ({ getValue }) => {
      const status = getValue()
      return (
        <Badge color={status === "published" ? "green" : "grey"} size="xsmall">
          {status === "published" ? "Published" : "Draft"}
        </Badge>
      )
    },
  }),
]
```

`createDataTableColumnHelper` utility creates a column helper that helps you define the columns for the data table. The column helper has an `accessor` method that accepts two parameters:

1. The column's key in the table's data.
2. An object with the following properties:
   - `header`: The column's header.
   - `cell`: (optional) By default, a data's value for a column is displayed as a string. Use this property to specify custom rendering of the value. It accepts a function that returns a string or a React node. The function receives an object that has a `getValue` property function to retrieve the raw value of the cell.
   - `enableSorting`: (optional) A boolean that enables sorting data by this column.
   - `sortLabel`: (optional) The label for the sorting button. If omitted, the `header` will be used instead if it's a string, otherwise the accessor key (id) will be used.
   - `sortAscLabel`: (optional) The label for the ascending sorting button. If omitted, the default value will be "A-Z".
   - `sortDescLabel`: (optional) The label for the descending sorting button. If omitted, the default value will be "Z-A".

Next, you'll define the filters that can be applied to the data table. You'll configure filtering by product status.

To define the filters, add the following:

```tsx title="src/admin/routes/custom/page.tsx"
// other imports...
import {
  // ...
  createDataTableFilterHelper,
} from "@medusajs/ui"

const filterHelper = createDataTableFilterHelper<HttpTypes.AdminProduct>()

const filters = [
  filterHelper.accessor("status", {
    type: "select",
    label: "Status",
    options: [
      {
        label: "Published",
        value: "published",
      },
      {
        label: "Draft",
        value: "draft",
      },
    ],
  }),
]
```

`createDataTableFilterHelper` utility creates a filter helper that helps you define the filters for the data table. The filter helper has an `accessor` method that accepts two parameters:

1. The key of a column in the table's data.
2. An object with the following properties:
   - `type`: The type of filter. It can be either:
     - `select`: A select dropdown allowing users to choose multiple values.
     - `radio`: A radio button allowing users to choose one value.
     - `date`: A date picker allowing users to choose a date.
   - `label`: The filter's label.
   - `options`: An array of objects with `label` and `value` properties. The `label` is the option's label, and the `value` is the value to filter by.

You'll now start creating the UI widget's component. Start by adding the necessary state variables:

```tsx title="src/admin/routes/custom/page.tsx"
// other imports...
import {
  // ...
  DataTablePaginationState,
  DataTableFilteringState,
  DataTableSortingState,
} from "@medusajs/ui"
import { useMemo, useState } from "react"

// ...

const limit = 15

const CustomPage = () => {
  const [pagination, setPagination] = useState<DataTablePaginationState>({
    pageSize: limit,
    pageIndex: 0,
  })
	const [search, setSearch] = useState<string>("")
	const [filtering, setFiltering] = useState<DataTableFilteringState>({})
  const [sorting, setSorting] = useState<DataTableSortingState | null>(null)

  const offset = useMemo(() => {
    return pagination.pageIndex * limit
  }, [pagination])
  const statusFilters = useMemo(() => {
    return (filtering.status || []) as ProductStatus
  }, [filtering])

  // TODO add data fetching logic
}
```

In the component, you've added the following state variables:

- `pagination`: An object of type `DataTablePaginationState` that holds the pagination state. It has two properties:
  - `pageSize`: The number of items to show per page.
  - `pageIndex`: The current page index.
- `search`: A string that holds the search query.
- `filtering`: An object of type `DataTableFilteringState` that holds the filtering state.
- `sorting`: An object of type `DataTableSortingState` that holds the sorting state.

You've also added two memoized variables:

- `offset`: How many items to skip when fetching data based on the current page.
- `statusFilters`: The selected status filters, if any.

Next, you'll fetch the products from the Medusa application. Assuming you have the JS SDK configured as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), add the following imports at the top of the file:

```tsx title="src/admin/routes/custom/page.tsx"
import { sdk } from "../../lib/config"
import { useQuery } from "@tanstack/react-query"
```

This imports the JS SDK instance and `useQuery` from [Tanstack Query](https://tanstack.com/query/latest).

Then, replace the `TODO` in the component with the following:

```tsx title="src/admin/routes/custom/page.tsx"
const { data, isLoading } = useQuery({
  queryFn: () => sdk.admin.product.list({
    limit,
    offset,
    q: search,
    status: statusFilters,
    order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined,
  }),
  queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]],
})

// TODO configure data table
```

You use the `useQuery` hook to fetch the products from the Medusa application. In the `queryFn`, you call the `sdk.admin.product.list` method to fetch the products. You pass the following query parameters to the method:

- `limit`: The number of products to fetch per page.
- `offset`: The number of products to skip based on the current page.
- `q`: The search query, if set.
- `status`: The status filters, if set.
- `order`: The sorting order, if set.

So, whenever the user changes the current page, search query, status filters, or sorting, the products are fetched based on the new parameters.

Next, you'll configure the data table. Medusa UI provides a `useDataTable` hook that helps you configure the data table. Add the following imports at the top of the file:

```tsx title="src/admin/routes/custom/page.tsx"
import {
  // ...
  useDataTable,
} from "@medusajs/ui"
import { useNavigate } from "react-router-dom"
```

Then, replace the `TODO` in the component with the following:

```tsx title="src/admin/routes/custom/page.tsx"
const navigate = useNavigate()

const table = useDataTable({
  columns,
  data: data?.products || [],
  getRowId: (row) => row.id,
  rowCount: data?.count || 0,
  isLoading,
  pagination: {
    state: pagination,
    onPaginationChange: setPagination,
  },
  search: {
    state: search,
    onSearchChange: setSearch,
  },
  filtering: {
    state: filtering,
    onFilteringChange: setFiltering,
  },
  filters,
  sorting: {
    // Pass the pagination state and updater to the table instance
    state: sorting,
    onSortingChange: setSorting,
  },
  onRowClick: (event, row) => {
    // Handle row click, for example
    navigate(`/products/${row.id}`)
  },
})

// TODO render component
```

The `useDataTable` hook accepts an object with the following properties:

- columns: (\`array\`) The columns to display in the data table. You created this using the \`createDataTableColumnHelper\` utility.
- data: (\`array\`) The products fetched from the Medusa application.
- getRowId: (\`function\`) A function that returns the unique ID of a row.
- rowCount: (\`number\`) The total number of products that can be retrieved. This is used to determine the number of pages.
- isLoading: (\`boolean\`) A boolean that indicates if the data is being fetched.
- pagination: (\`object\`) An object to configure pagination.

  - state: (\`object\`) The pagination React state variable.

  - onPaginationChange: (\`function\`) A function that updates the pagination state.
- search: (\`object\`) An object to configure searching.

  - state: (\`string\`) The search query React state variable.

  - onSearchChange: (\`function\`) A function that updates the search query state.
- filtering: (\`object\`) An object to configure filtering.

  - state: (\`object\`) The filtering React state variable.

  - onFilteringChange: (\`function\`) A function that updates the filtering state.
- filters: (\`array\`) The filters to display in the data table. You created this using the \`createDataTableFilterHelper\` utility.
- sorting: (\`object\`) An object to configure sorting.

  - state: (\`object\`) The sorting React state variable.

  - onSortingChange: (\`function\`) A function that updates the sorting state.
- onRowClick: (\`function\`) A function that allows you to perform an action when the user clicks on a row. In this example, you navigate to the product's detail page.

  - event: (\`mouseevent\`) An instance of the \[MouseClickEvent]\(https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) object.

  - row: (\`object\`) The data of the row that was clicked.

Finally, you'll render the data table. But first, add the following imports at the top of the page:

```tsx title="src/admin/routes/custom/page.tsx"
import {
  // ...
  DataTable,
} from "@medusajs/ui"
import { SingleColumnLayout } from "../../layouts/single-column"
import { Container } from "../../components/container"
```

Aside from the `DataTable` component, you also import the [SingleColumnLayout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/layouts/single-column/index.html.md) and [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) components implemented in other Admin Component guides. These components ensure a style consistent to other pages in the admin dashboard.

Then, replace the `TODO` in the component with the following:

```tsx title="src/admin/routes/custom/page.tsx"
return (
  <SingleColumnLayout>
    <Container>
      <DataTable instance={table}>
        <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
          <Heading>Products</Heading>
          <div className="flex gap-2">
            <DataTable.FilterMenu tooltip="Filter" />
            <DataTable.SortingMenu tooltip="Sort" />
            <DataTable.Search placeholder="Search..." />
          </div>
        </DataTable.Toolbar>
        <DataTable.Table />
        <DataTable.Pagination />
      </DataTable>
    </Container>
  </SingleColumnLayout>
)
```

You render the `DataTable` component and pass the `table` instance as a prop. In the `DataTable` component, you render a toolbar showing a heading, filter menu, sorting menu, and a search input. You also show pagination after the table.

Lastly, export the component and the UI widget's configuration at the end of the file:

```tsx title="src/admin/routes/custom/page.tsx"
// other imports...
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"

// ...

export const config = defineRouteConfig({
  label: "Custom",
  icon: ChatBubbleLeftRight,
})

export default CustomPage
```

If you start your Medusa application and go to `localhost:9000/app/custom`, you'll see the data table showing the list of products with pagination, filtering, searching, and sorting functionalities.

### Full Example Code

```tsx title="src/admin/routes/custom/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { 
  Badge,
  createDataTableColumnHelper,
  createDataTableFilterHelper,
  DataTable,
  DataTableFilteringState,
  DataTablePaginationState,
  DataTableSortingState,
  Heading,
  useDataTable,
} from "@medusajs/ui"
import { useQuery } from "@tanstack/react-query"
import { SingleColumnLayout } from "../../layouts/single-column"
import { sdk } from "../../lib/config"
import { useMemo, useState } from "react"
import { Container } from "../../components/container"
import { HttpTypes, ProductStatus } from "@medusajs/framework/types"

const columnHelper = createDataTableColumnHelper<HttpTypes.AdminProduct>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    // Enables sorting for the column.
    enableSorting: true,
    // If omitted, the header will be used instead if it's a string, 
    // otherwise the accessor key (id) will be used.
    sortLabel: "Title",
    // If omitted the default value will be "A-Z"
    sortAscLabel: "A-Z",
    // If omitted the default value will be "Z-A"
    sortDescLabel: "Z-A",
  }),
  columnHelper.accessor("status", {
    header: "Status",
    cell: ({ getValue }) => {
      const status = getValue()
      return (
        <Badge color={status === "published" ? "green" : "grey"} size="xsmall">
          {status === "published" ? "Published" : "Draft"}
        </Badge>
      )
    },
  }),
]

const filterHelper = createDataTableFilterHelper<HttpTypes.AdminProduct>()

const filters = [
  filterHelper.accessor("status", {
    type: "select",
    label: "Status",
    options: [
      {
        label: "Published",
        value: "published",
      },
      {
        label: "Draft",
        value: "draft",
      },
    ],
  }),
]

const limit = 15

const CustomPage = () => {
  const [pagination, setPagination] = useState<DataTablePaginationState>({
    pageSize: limit,
    pageIndex: 0,
  })
	const [search, setSearch] = useState<string>("")
	const [filtering, setFiltering] = useState<DataTableFilteringState>({})
  const [sorting, setSorting] = useState<DataTableSortingState | null>(null)

  const offset = useMemo(() => {
    return pagination.pageIndex * limit
  }, [pagination])
  const statusFilters = useMemo(() => {
    return (filtering.status || []) as ProductStatus
  }, [filtering])

  const { data, isLoading } = useQuery({
    queryFn: () => sdk.admin.product.list({
      limit,
      offset,
      q: search,
      status: statusFilters,
      order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined,
    }),
    queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]],
  })

  const table = useDataTable({
    columns,
    data: data?.products || [],
    getRowId: (row) => row.id,
    rowCount: data?.count || 0,
    isLoading,
    pagination: {
      state: pagination,
      onPaginationChange: setPagination,
    },
    search: {
	    state: search,
	    onSearchChange: setSearch,
    },
    filtering: {
      state: filtering,
      onFilteringChange: setFiltering,
    },
    filters,
    sorting: {
      // Pass the pagination state and updater to the table instance
      state: sorting,
      onSortingChange: setSorting,
    },
  })

  return (
    <SingleColumnLayout>
      <Container>
        <DataTable instance={table}>
          <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
            <Heading>Products</Heading>
            <div className="flex gap-2">
              <DataTable.FilterMenu tooltip="Filter" />
              <DataTable.SortingMenu tooltip="Sort" />
              <DataTable.Search placeholder="Search..." />
            </div>
          </DataTable.Toolbar>
          <DataTable.Table />
          <DataTable.Pagination />
        </DataTable>
      </Container>
    </SingleColumnLayout>
  )
}

export const config = defineRouteConfig({
  label: "Custom",
  icon: ChatBubbleLeftRight,
})

export default CustomPage
```


# Forms - Admin Components

In this guide, you'll learn how to create forms that match the Medusa Admin's design conventions.

The Medusa Admin has two types of forms:

1. Create forms, created using the [FocusModal UI component](https://docs.medusajs.com/ui/components/focus-modal/index.html.md).
2. Edit or update forms, created using the [Drawer UI component](https://docs.medusajs.com/ui/components/drawer/index.html.md).

This guide explains how to create these two form types following the Medusa Admin's conventions.

## Form Tooling

The Medusa Admin uses the following tools to build the forms:

1. [react-hook-form](https://react-hook-form.com/) to easily build forms and manage their states.
2. [Zod](https://zod.dev/) to validate the form's fields.

Both of these libraries are available in your project, so you don't have to install them to use them.

***

## Create Form

In this section, you'll build a form component to create an item of a resource.

### Full Component

```tsx title="src/admin/components/create-form.tsx"
import { 
  FocusModal,
  Heading,
  Label,
  Input,
  Button,
} from "@medusajs/ui"
import { 
  useForm, 
  FormProvider,
  Controller,
} from "react-hook-form"
import * as zod from "zod"

const schema = zod.object({
  name: zod.string(),
})

export const CreateForm = () => {
  const form = useForm<zod.infer<typeof schema>>({
    defaultValues: {
      name: "",
    },
  })

  const handleSubmit = form.handleSubmit(({ name }) => {
    // TODO: submit to backend
    console.log(name)
  })

  return (
    <FocusModal>
      <FocusModal.Trigger asChild>
        <Button>Create</Button>
      </FocusModal.Trigger>
      <FocusModal.Content>
        <FormProvider {...form}>
          <form
            onSubmit={handleSubmit}
            className="flex h-full flex-col overflow-hidden"
          >
            <FocusModal.Header>
              <div className="flex items-center justify-end gap-x-2">
                  <FocusModal.Close asChild>
                    <Button size="small" variant="secondary">
                      Cancel
                    </Button>
                  </FocusModal.Close>
                  <Button type="submit" size="small">
                    Save
                  </Button>
                </div>
            </FocusModal.Header>
            <FocusModal.Body>
                <div className="flex flex-1 flex-col items-center overflow-y-auto">
                  <div className="mx-auto flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
                    <div>
                      <Heading className="capitalize">
                        Create Item
                      </Heading>
                    </div>
                    <div className="grid grid-cols-2 gap-4">
                      <Controller
                        control={form.control}
                        name="name"
                        render={({ field }) => {
                          return (
                            <div className="flex flex-col space-y-2">
                              <div className="flex items-center gap-x-1">
                                <Label size="small" weight="plus">
                                  Name
                                </Label>
                              </div>
                              <Input {...field} />
                            </div>
                          )
                        }}
                      />
                    </div>
                  </div>
                </div>
            </FocusModal.Body>
          </form>
        </FormProvider>
      </FocusModal.Content>
    </FocusModal>
  )
}
```

Unlike other components in this documentation, this form component isn't reusable. You have to create one for every resource that has a create form in the admin.

Start by creating the file `src/admin/components/create-form.tsx` in which you'll create the form.

### Create Validation Schema

In `src/admin/components/create-form.tsx`, create a validation schema with Zod for the form's fields:

```tsx title="src/admin/components/create-form.tsx"
import * as zod from "zod"

const schema = zod.object({
  name: zod.string(),
})
```

The form in this guide is simple, it only has a required `name` field, which is a string.

### Initialize Form

Next, you'll initialize the form using `react-hook-form`.

Add to `src/admin/components/create-form.tsx` the following:

```tsx title="src/admin/components/create-form.tsx"
// other imports...
import { useForm } from "react-hook-form"

// validation schema...

export const CreateForm = () => {
  const form = useForm<zod.infer<typeof schema>>({
    defaultValues: {
      name: "",
    },
  })

  const handleSubmit = form.handleSubmit(({ name }) => {
    // TODO: submit to backend
    console.log(name)
  })

  // TODO render form
}
```

You create the `CreateForm` component. For now, it uses `useForm` from `react-hook-form` to initialize a form.

You also define a `handleSubmit` function to perform an action when the form is submitted.

You can replace the content of the function with sending a request to Medusa's routes. Refer to [this guide](https://docs.medusajs.com/docs/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md) for more details on how to do that.

### Render Components

You'll now add a `return` statement that renders the focus modal where the form is shown.

Replace `// TODO render form` with the following:

```tsx title="src/admin/components/create-form.tsx"
// other imports...
import { 
  FocusModal,
  Heading,
  Label,
  Input,
  Button,
} from "@medusajs/ui"
import { 
  FormProvider,
  Controller,
} from "react-hook-form"

export const CreateForm = () => {
  // ...

  return (
    <FocusModal>
      <FocusModal.Trigger asChild>
        <Button>Create</Button>
      </FocusModal.Trigger>
      <FocusModal.Content>
        <FormProvider {...form}>
          <form
            onSubmit={handleSubmit}
            className="flex h-full flex-col overflow-hidden"
          >
            <FocusModal.Header>
              <div className="flex items-center justify-end gap-x-2">
                  <FocusModal.Close asChild>
                    <Button size="small" variant="secondary">
                      Cancel
                    </Button>
                  </FocusModal.Close>
                  <Button type="submit" size="small">
                    Save
                  </Button>
                </div>
            </FocusModal.Header>
            <FocusModal.Body>
                <div className="flex flex-1 flex-col items-center overflow-y-auto">
                  <div className="mx-auto flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
                    <div>
                      <Heading className="capitalize">
                        Create Item
                      </Heading>
                    </div>
                    <div className="grid grid-cols-2 gap-4">
                      <Controller
                        control={form.control}
                        name="name"
                        render={({ field }) => {
                          return (
                            <div className="flex flex-col space-y-2">
                              <div className="flex items-center gap-x-1">
                                <Label size="small" weight="plus">
                                  Name
                                </Label>
                              </div>
                              <Input {...field} />
                            </div>
                          )
                        }}
                      />
                    </div>
                  </div>
                </div>
            </FocusModal.Body>
          </form>
        </FormProvider>
      </FocusModal.Content>
    </FocusModal>
  )
}
```

You render a focus modal, with a trigger button to open it.

In the `FocusModal.Content` component, you wrap the content with the `FormProvider` component from `react-hook-form`, passing it the details of the form you initialized earlier as props.

In the `FormProvider`, you add a `form` component passing it the `handleSubmit` function you created earlier as the handler of the `onSubmit` event.

In the `FocusModal.Header` component, you add buttons to save or cancel the form submission.

Finally, you render the form's components inside the `FocusModal.Body`. To render inputs, you use the `Controller` component imported from `react-hook-form`.

### Use Create Form Component

You can use the `CreateForm` component in your widget or UI route.

For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:

```tsx title="src/admin/widgets/product-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { CreateForm } from "../components/create-form"
import { Container } from "../components/container"
import { Header } from "../components/header"

const ProductWidget = () => {
  return (
    <Container>
      <Header
        title="Items"
        actions={[
          {
            type: "custom",
            children: <CreateForm />,
          },
        ]}
      />
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

This component uses the [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom components.

It will add at the top of a product's details page a new section, and in its header you'll find a Create button. If you click on it, it will open the focus modal with your form.

***

## Edit Form

In this section, you'll build a form component to edit an item of a resource.

### Full Component

```tsx title="src/admin/components/edit-form.tsx"
import { 
  Drawer,
  Heading,
  Label,
  Input,
  Button,
} from "@medusajs/ui"
import { 
  useForm, 
  FormProvider,
  Controller,
} from "react-hook-form"
import * as zod from "zod"

const schema = zod.object({
  name: zod.string(),
})

export const EditForm = () => {
  const form = useForm<zod.infer<typeof schema>>({
    defaultValues: {
      name: "",
    },
  })

  const handleSubmit = form.handleSubmit(({ name }) => {
    // TODO: submit to backend
    console.log(name)
  })

  return (
    <Drawer>
      <Drawer.Trigger asChild>
        <Button>Edit Item</Button>
      </Drawer.Trigger>
      <Drawer.Content>
        <FormProvider {...form}>
          <form
            onSubmit={handleSubmit}
            className="flex flex-1 flex-col overflow-hidden"
          >
          <Drawer.Header>
            <Heading className="capitalize">
              Edit Item
            </Heading>
          </Drawer.Header>
          <Drawer.Body className="flex max-w-full flex-1 flex-col gap-y-8 overflow-y-auto">
            <Controller
              control={form.control}
              name="name"
              render={({ field }) => {
                return (
                  <div className="flex flex-col space-y-2">
                    <div className="flex items-center gap-x-1">
                      <Label size="small" weight="plus">
                        Name
                      </Label>
                    </div>
                    <Input {...field} />
                  </div>
                )
              }}
            />
          </Drawer.Body>
          <Drawer.Footer>
            <div className="flex items-center justify-end gap-x-2">
              <Drawer.Close asChild>
                <Button size="small" variant="secondary">
                  Cancel
                </Button>
              </Drawer.Close>
              <Button size="small" type="submit">
                Save
              </Button>
            </div>
          </Drawer.Footer>
          </form>
        </FormProvider>
      </Drawer.Content>
    </Drawer>
  )
}
```

Unlike other components in this documentation, this form component isn't reusable. You have to create one for every resource that has an edit form in the admin.

Start by creating the file `src/admin/components/edit-form.tsx` in which you'll create the form.

### Create Validation Schema

In `src/admin/components/edit-form.tsx`, create a validation schema with Zod for the form's fields:

```tsx title="src/admin/components/edit-form.tsx"
import * as zod from "zod"

const schema = zod.object({
  name: zod.string(),
})
```

The form in this guide is simple, it only has a required `name` field, which is a string.

### Initialize Form

Next, you'll initialize the form using `react-hook-form`.

Add to `src/admin/components/edit-form.tsx` the following:

```tsx title="src/admin/components/edit-form.tsx"
// other imports...
import { useForm } from "react-hook-form"

// validation schema...

export const EditForm = () => {
  const form = useForm<zod.infer<typeof schema>>({
    defaultValues: {
      name: "",
    },
  })

  const handleSubmit = form.handleSubmit(({ name }) => {
    // TODO: submit to backend
    console.log(name)
  })

  // TODO render form
}
```

You create the `EditForm` component. For now, it uses `useForm` from `react-hook-form` to initialize a form.

You also define a `handleSubmit` function to perform an action when the form is submitted.

You can replace the content of the function with sending a request to Medusa's routes. Refer to [this guide](https://docs.medusajs.com/docs/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md) for more details on how to do that.

### Render Components

You'll now add a `return` statement that renders the drawer where the form is shown.

Replace `// TODO render form` with the following:

```tsx title="src/admin/components/edit-form.tsx"
// other imports...
import { 
  Drawer,
  Heading,
  Label,
  Input,
  Button,
} from "@medusajs/ui"
import { 
  FormProvider,
  Controller,
} from "react-hook-form"

export const EditForm = () => {
  // ...

  return (
    <Drawer>
      <Drawer.Trigger asChild>
        <Button>Edit Item</Button>
      </Drawer.Trigger>
      <Drawer.Content>
        <FormProvider {...form}>
          <form
            onSubmit={handleSubmit}
            className="flex flex-1 flex-col overflow-hidden"
          >
          <Drawer.Header>
            <Heading className="capitalize">
              Edit Item
            </Heading>
          </Drawer.Header>
          <Drawer.Body className="flex max-w-full flex-1 flex-col gap-y-8 overflow-y-auto">
            <Controller
              control={form.control}
              name="name"
              render={({ field }) => {
                return (
                  <div className="flex flex-col space-y-2">
                    <div className="flex items-center gap-x-1">
                      <Label size="small" weight="plus">
                        Name
                      </Label>
                    </div>
                    <Input {...field} />
                  </div>
                )
              }}
            />
          </Drawer.Body>
          <Drawer.Footer>
            <div className="flex items-center justify-end gap-x-2">
              <Drawer.Close asChild>
                <Button size="small" variant="secondary">
                  Cancel
                </Button>
              </Drawer.Close>
              <Button size="small" type="submit">
                Save
              </Button>
            </div>
          </Drawer.Footer>
          </form>
        </FormProvider>
      </Drawer.Content>
    </Drawer>
  )
}
```

You render a drawer, with a trigger button to open it.

In the `Drawer.Content` component, you wrap the content with the `FormProvider` component from `react-hook-form`, passing it the details of the form you initialized earlier as props.

In the `FormProvider`, you add a `form` component passing it the `handleSubmit` function you created earlier as the handler of the `onSubmit` event.

You render the form's components inside the `Drawer.Body`. To render inputs, you use the `Controller` component imported from `react-hook-form`.

Finally, in the `Drawer.Footer` component, you add buttons to save or cancel the form submission.

### Use Edit Form Component

You can use the `EditForm` component in your widget or UI route.

For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:

```tsx title="src/admin/widgets/product-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container } from "../components/container"
import { Header } from "../components/header"
import { EditForm } from "../components/edit-form"

const ProductWidget = () => {
  return (
    <Container>
      <Header
        title="Items"
        actions={[
          {
            type: "custom",
            children: <EditForm />,
          },
        ]}
      />
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

This component uses the [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom components.

It will add at the top of a product's details page a new section, and in its header you'll find an "Edit Item" button. If you click on it, it will open the drawer with your form.


# Header - Admin Components

In this guide, you'll learn how to create a header component that matches the Medusa Admin's design conventions.

Each section in the Medusa Admin has a header with a title, and optionally a subtitle with buttons to perform an action.

![Example of a header in a section](https://res.cloudinary.com/dza7lstvk/image/upload/v1728288562/Medusa%20Resources/header_dtz4gl.png)

To create a component that uses the same header styling and structure, create the file `src/admin/components/header.tsx` with the following content:

```tsx title="src/admin/components/header.tsx"
import { Heading, Button, Text } from "@medusajs/ui"
import React from "react"
import { Link, LinkProps } from "react-router-dom"
import { ActionMenu, ActionMenuProps } from "./action-menu"

export type HeadingProps = {
  title: string
  subtitle?: string
  actions?: (
    {
      type: "button",
      props: React.ComponentProps<typeof Button>
      link?: LinkProps
    } |  
    {
      type: "action-menu"
      props: ActionMenuProps
    } |
    {
      type: "custom"
      children: React.ReactNode
    }
  )[]
}

export const Header = ({
  title,
  subtitle,
  actions = [],
}: HeadingProps) => {
  return (
    <div className="flex items-center justify-between px-6 py-4">
      <div>
        <Heading level="h2">{title}</Heading>
        {subtitle && (
          <Text className="text-ui-fg-subtle" size="small">
            {subtitle}
          </Text>
        )}
      </div>
      {actions.length > 0 && (
        <div className="flex items-center justify-center gap-x-2">
          {actions.map((action, index) => (
            <>
              {action.type === "button" && (
                <Button 
                  {...action.props} 
                  size={action.props.size || "small"}
                  key={index}
                >
                  <>
                    {action.props.children}
                    {action.link && <Link {...action.link} />}
                  </>
                </Button>
              )}
              {action.type === "action-menu" && (
                <ActionMenu {...action.props} />
              )}
              {action.type === "custom" && action.children}
            </>
          ))}
        </div>
      )}
    </div>
  )
}
```

The `Header` component shows a title, and optionally a subtitle and action buttons.

The component also uses the [Action Menu](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/action-menu/index.html.md) custom component.

It accepts the following props:

- title: (\`string\`) The section's title.
- subtitle: (\`string\`) The section's subtitle.
- actions: (\`object\[]\`) An array of actions to show.

  - type: (\`button\` \\| \`action-menu\` \\| \`custom\`) The type of action to add.

    \- If its value is \`button\`, it'll show a button that can have a link or an on-click action.

    \- If its value is \`action-menu\`, it'll show a three dot icon with a dropdown of actions.

    \- If its value is \`custom\`, you can pass any React nodes to render.

  - props: (object) This property is only accepted if \`type\` is \`button\` or \`action-menu\`. If \`type\` is \`button\`, it accepts the \[props to pass to the UI Button component]\(https://docs.medusajs.com/components/button). If \`type\` is \`action-menu\`, it accepts the props to pass to the action menu, explained in \[this guide]\(../action-menu/page.mdx).

  - link: (\[LinkProps]\(https://reactrouter.com/en/main/components/link)) This property is only accepted if \`type\` is \`button\`. If provided, a link is rendered inside the button. Its value is the props to pass the \`Link\` component of \`react-router-dom\`.

  - children: (React.ReactNode) This property is only accepted if \`type\` is \`custom\`. Its content is rendered as part of the actions.

***

## Example

Use the `Header` component in any widget or UI route.

For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:

```tsx title="src/admin/widgets/product-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container } from "../components/container"
import { Header } from "../components/header"

const ProductWidget = () => {
  return (
    <Container>
      <Header 
        title="Product Widget"
        subtitle="This is my custom product widget"
        actions={[
          {
            type: "button",
            props: {
              children: "Click me",
              variant: "secondary",
              onClick: () => {
                alert("You clicked the button.")
              },
            },
          },
        ]}
      />
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

This widget also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) custom component.


# JSON View - Admin Components

In this guide, you'll learn how to create a JSON view section that matches the Medusa Admin's design conventions.

Detail pages in the Medusa Admin show a JSON section to view the current page's details in JSON format.

![Example of a JSON section in the admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295129/Medusa%20Resources/json_dtbsgm.png)

To create a component that shows a JSON section in your customizations, create the file `src/admin/components/json-view-section.tsx` with the following content:

```tsx title="src/admin/components/json-view-section.tsx"
import {
  ArrowUpRightOnBox,
  Check,
  SquareTwoStack,
  TriangleDownMini,
  XMarkMini,
} from "@medusajs/icons"
import {
  Badge,
  Container,
  Drawer,
  Heading,
  IconButton,
  Kbd,
} from "@medusajs/ui"
import Primitive from "@uiw/react-json-view"
import { CSSProperties, MouseEvent, Suspense, useState } from "react"

type JsonViewSectionProps = {
  data: object
  title?: string
}

export const JsonViewSection = ({ data }: JsonViewSectionProps) => {
  const numberOfKeys = Object.keys(data).length

  return (
    <Container className="flex items-center justify-between px-6 py-4">
      <div className="flex items-center gap-x-4">
        <Heading level="h2">JSON</Heading>
        <Badge size="2xsmall" rounded="full">
          {numberOfKeys} keys
        </Badge>
      </div>
      <Drawer>
        <Drawer.Trigger asChild>
          <IconButton
            size="small"
            variant="transparent"
            className="text-ui-fg-muted hover:text-ui-fg-subtle"
          >
            <ArrowUpRightOnBox />
          </IconButton>
        </Drawer.Trigger>
        <Drawer.Content className="bg-ui-contrast-bg-base text-ui-code-fg-subtle !shadow-elevation-commandbar overflow-hidden border border-none max-md:inset-x-2 max-md:max-w-[calc(100%-16px)]">
          <div className="bg-ui-code-bg-base flex items-center justify-between px-6 py-4">
            <div className="flex items-center gap-x-4">
              <Drawer.Title asChild>
                <Heading className="text-ui-contrast-fg-primary">
                <span className="text-ui-fg-subtle">
                  {numberOfKeys}
                </span>
                </Heading>
              </Drawer.Title>
            </div>
            <div className="flex items-center gap-x-2">
              <Kbd className="bg-ui-contrast-bg-subtle border-ui-contrast-border-base text-ui-contrast-fg-secondary">
                esc
              </Kbd>
              <Drawer.Close asChild>
                <IconButton
                  size="small"
                  variant="transparent"
                  className="text-ui-contrast-fg-secondary hover:text-ui-contrast-fg-primary hover:bg-ui-contrast-bg-base-hover active:bg-ui-contrast-bg-base-pressed focus-visible:bg-ui-contrast-bg-base-hover focus-visible:shadow-borders-interactive-with-active"
                >
                  <XMarkMini />
                </IconButton>
              </Drawer.Close>
            </div>
          </div>
          <Drawer.Body className="flex flex-1 flex-col overflow-hidden px-[5px] py-0 pb-[5px]">
            <div className="bg-ui-contrast-bg-subtle flex-1 overflow-auto rounded-b-[4px] rounded-t-lg p-3">
              <Suspense
                fallback={<div className="flex size-full flex-col"></div>}
              >
                <Primitive
                  value={data}
                  displayDataTypes={false}
                  style={
                    {
                      "--w-rjv-font-family": "Roboto Mono, monospace",
                      "--w-rjv-line-color": "var(--contrast-border-base)",
                      "--w-rjv-curlybraces-color":
                        "var(--contrast-fg-secondary)",
                      "--w-rjv-brackets-color": "var(--contrast-fg-secondary)",
                      "--w-rjv-key-string": "var(--contrast-fg-primary)",
                      "--w-rjv-info-color": "var(--contrast-fg-secondary)",
                      "--w-rjv-type-string-color": "var(--tag-green-icon)",
                      "--w-rjv-quotes-string-color": "var(--tag-green-icon)",
                      "--w-rjv-type-boolean-color": "var(--tag-orange-icon)",
                      "--w-rjv-type-int-color": "var(--tag-orange-icon)",
                      "--w-rjv-type-float-color": "var(--tag-orange-icon)",
                      "--w-rjv-type-bigint-color": "var(--tag-orange-icon)",
                      "--w-rjv-key-number": "var(--contrast-fg-secondary)",
                      "--w-rjv-arrow-color": "var(--contrast-fg-secondary)",
                      "--w-rjv-copied-color": "var(--contrast-fg-secondary)",
                      "--w-rjv-copied-success-color":
                        "var(--contrast-fg-primary)",
                      "--w-rjv-colon-color": "var(--contrast-fg-primary)",
                      "--w-rjv-ellipsis-color": "var(--contrast-fg-secondary)",
                    } as CSSProperties
                  }
                  collapsed={1}
                >
                  <Primitive.Quote render={() => <span />} />
                  <Primitive.Null
                    render={() => (
                      <span className="text-ui-tag-red-icon">null</span>
                    )}
                  />
                  <Primitive.Undefined
                    render={() => (
                      <span className="text-ui-tag-blue-icon">undefined</span>
                    )}
                  />
                  <Primitive.CountInfo
                    render={(_props, { value }) => {
                      return (
                        <span className="text-ui-contrast-fg-secondary ml-2">
                          {Object.keys(value as object).length} items
                        </span>
                      )
                    }}
                  />
                  <Primitive.Arrow>
                    <TriangleDownMini className="text-ui-contrast-fg-secondary -ml-[0.5px]" />
                  </Primitive.Arrow>
                  <Primitive.Colon>
                    <span className="mr-1">:</span>
                  </Primitive.Colon>
                  <Primitive.Copied
                    render={({ style }, { value }) => {
                      return <Copied style={style} value={value} />
                    }}
                  />
                </Primitive>
              </Suspense>
            </div>
          </Drawer.Body>
        </Drawer.Content>
      </Drawer>
    </Container>
  )
}

type CopiedProps = {
  style?: CSSProperties
  value: object | undefined
}

const Copied = ({ style, value }: CopiedProps) => {
  const [copied, setCopied] = useState(false)

  const handler = (e: MouseEvent<HTMLSpanElement>) => {
    e.stopPropagation()
    setCopied(true)

    if (typeof value === "string") {
      navigator.clipboard.writeText(value)
    } else {
      const json = JSON.stringify(value, null, 2)
      navigator.clipboard.writeText(json)
    }

    setTimeout(() => {
      setCopied(false)
    }, 2000)
  }

  const styl = { whiteSpace: "nowrap", width: "20px" }

  if (copied) {
    return (
      <span style={{ ...style, ...styl }}>
        <Check className="text-ui-contrast-fg-primary" />
      </span>
    )
  }

  return (
    <span style={{ ...style, ...styl }} onClick={handler}>
      <SquareTwoStack className="text-ui-contrast-fg-secondary" />
    </span>
  )
}
```

The `JsonViewSection` component shows a section with the "JSON" title and a button to show the data as JSON in a drawer or side window.

The `JsonViewSection` accepts a `data` prop, which is the data to show as a JSON object in the drawer.

***

## Example

Use the `JsonViewSection` component in any widget or UI route.

For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:

```tsx title="src/admin/widgets/product-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { JsonViewSection } from "../components/json-view-section"

const ProductWidget = () => {
  return <JsonViewSection data={{
    name: "John",
  }} />
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

This shows the JSON section at the top of the product page, passing it the object `{ name: "John" }`.


# Section Row - Admin Components

In this guide, you'll learn how to create a section row component that matches the Medusa Admin's design conventions.

The Medusa Admin often shows information in rows of label-values, such as when showing a product's details.

![Example of a section row in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728292781/Medusa%20Resources/section-row_kknbnw.png)

To create a component that shows information in the same structure, create the file `src/admin/components/section-row.tsx` with the following content:

```tsx title="src/admin/components/section-row.tsx"
import { Text, clx } from "@medusajs/ui"

export type SectionRowProps = {
  title: string
  value?: React.ReactNode | string | null
  actions?: React.ReactNode
}

export const SectionRow = ({ title, value, actions }: SectionRowProps) => {
  const isValueString = typeof value === "string" || !value

  return (
    <div
      className={clx(
        `text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4`,
        {
          "grid-cols-[1fr_1fr_28px]": !!actions,
        }
      )}
    >
      <Text size="small" weight="plus" leading="compact">
        {title}
      </Text>

      {isValueString ? (
        <Text
          size="small"
          leading="compact"
          className="whitespace-pre-line text-pretty"
        >
          {value ?? "-"}
        </Text>
      ) : (
        <div className="flex flex-wrap gap-1">{value}</div>
      )}

      {actions && <div>{actions}</div>}
    </div>
  )
}
```

The `SectionRow` component shows a title and a value in the same row.

It accepts the following props:

- title: (\`string\`) The title to show on the left side.
- value: (\`React.ReactNode\` \\| \`string\` \\| \`null\`) The value to show on the right side.
- actions: (\`React.ReactNode\`) The actions to show at the end of the row.

***

## Example

Use the `SectionRow` component in any widget or UI route.

For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:

```tsx title="src/admin/widgets/product-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container } from "../components/container"
import { Header } from "../components/header"
import { SectionRow } from "../components/section-row"

const ProductWidget = () => {
  return (
    <Container>
      <Header title="Product Widget" />
      <SectionRow title="Name" value="John" />
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

This widget also uses the [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component.


# Table - Admin Components

If you're using [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0), it's recommended to use the [Data Table](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/data-table/index.html.md) component instead as it provides features for sorting, filtering, pagination, and more with a simpler API.

You can use the `Table` component from Medusa UI to display data in a table. It's mostly recommended for simpler tables.

To create a component that shows a table with pagination, create the file `src/admin/components/table.tsx` with the following content:

```tsx title="src/admin/components/table.tsx"
import { useMemo } from "react"
import { Table as UiTable } from "@medusajs/ui"

export type TableProps = {
  columns: {
    key: string
    label?: string
    render?: (value: unknown) => React.ReactNode
  }[]
  data: Record<string, unknown>[]
  pageSize: number
  count: number
  currentPage: number
  setCurrentPage: (value: number) => void
}

export const Table = ({
  columns,
  data,
  pageSize,
  count,
  currentPage,
  setCurrentPage,
}: TableProps) => {
  const pageCount = useMemo(() => {
    return Math.ceil(count / pageSize)
  }, [data, pageSize])

  const canNextPage = useMemo(() => {
    return currentPage < pageCount - 1
  }, [currentPage, pageCount])
  const canPreviousPage = useMemo(() => {
    return currentPage - 1 >= 0
  }, [currentPage])

  const nextPage = () => {
    if (canNextPage) {
      setCurrentPage(currentPage + 1)
    }
  }

  const previousPage = () => {
    if (canPreviousPage) {
      setCurrentPage(currentPage - 1)
    }
  }

  return (
    <div className="flex h-full flex-col overflow-hidden !border-t-0">
      <UiTable>
        <UiTable.Header>
          <UiTable.Row>
            {columns.map((column, index) => (
              <UiTable.HeaderCell key={index}>
                {column.label || column.key}
              </UiTable.HeaderCell>
            ))}
          </UiTable.Row>
        </UiTable.Header>
        <UiTable.Body>
          {data.map((item, index) => {
            const rowIndex = "id" in item ? item.id as string : index
            return (
              <UiTable.Row key={rowIndex}>
                {columns.map((column, index) => (
                  <UiTable.Cell key={`${rowIndex}-${index}`}>
                    <>
                      {column.render && column.render(item[column.key])}
                      {!column.render && (
                        <>{item[column.key] as string}</>
                      )}
                    </>
                  </UiTable.Cell>
                ))}
              </UiTable.Row>
            )
          })}
        </UiTable.Body>
      </UiTable>
      <UiTable.Pagination
        count={count}
        pageSize={pageSize}
        pageIndex={currentPage}
        pageCount={pageCount}
        canPreviousPage={canPreviousPage}
        canNextPage={canNextPage}
        previousPage={previousPage}
        nextPage={nextPage}
      />
    </div>
  )
}
```

The `Table` component uses the component from the [UI package](https://docs.medusajs.com/ui/components/table/index.html.md), with additional styling and rendering of data.

It accepts the following props:

- columns: (\`object\[]\`) The table's columns.

  - key: (\`string\`) The column's key in the passed \`data\`

  - label: (\`string\`) The column's label shown in the table. If not provided, the \`key\` is used.

  - render: (\`(value: unknown) => React.ReactNode\`) By default, the data is shown as-is in the table. You can use this function to change how the value is rendered. The function receives the value is a parameter and returns a React node.
- data: (\`Record\<string, unknown>\[]\`) The data to show in the table for the current page. The keys of each object should be in the \`columns\` array.
- pageSize: (\`number\`) The number of items to show per page.
- count: (\`number\`) The total number of items.
- currentPage: (\`number\`) A zero-based index indicating the current page's number.
- setCurrentPage: (\`(value: number) => void\`) A function used to change the current page.

***

## Example

Use the `Table` component in any widget or UI route.

For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:

```tsx title="src/admin/widgets/product-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { StatusBadge } from "@medusajs/ui"
import { Table } from "../components/table"
import { useState } from "react"
import { Container } from "../components/container"

const ProductWidget = () => {
  const [currentPage, setCurrentPage] = useState(0)

  return (
    <Container>
      <Table
        columns={[
          {
            key: "name",
            label: "Name",
          },
          {
            key: "is_enabled",
            label: "Status",
            render: (value: unknown) => {
              const isEnabled = value as boolean

              return (
                <StatusBadge color={isEnabled ? "green" : "grey"}>
                  {isEnabled ? "Enabled" : "Disabled"}
                </StatusBadge>
              )
            },
          },
        ]}
        data={[
          {
            name: "John",
            is_enabled: true,
          },
          {
            name: "Jane",
            is_enabled: false,
          },
        ]}
        pageSize={2}
        count={2}
        currentPage={currentPage}
        setCurrentPage={setCurrentPage}
      />
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

This widget also uses the [Container](../container.mdx) custom component.

***

## Example With Data Fetching

This section shows you how to use the `Table` component when fetching data from the Medusa application's API routes.

Assuming you've set up the JS SDK as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), create the UI route `src/admin/routes/custom/page.tsx` with the following content:

```tsx title="src/admin/routes/custom/page.tsx" collapsibleLines="1-10" expandButtonLabel="Show Imports" highlights={tableExampleHighlights}
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { useQuery } from "@tanstack/react-query"
import { SingleColumnLayout } from "../../layouts/single-column"
import { Table } from "../../components/table"
import { sdk } from "../../lib/config"
import { useMemo, useState } from "react"
import { Container } from "../../components/container"
import { Header } from "../../components/header"

const CustomPage = () => {
  const [currentPage, setCurrentPage] = useState(0)
  const limit = 15
  const offset = useMemo(() => {
    return currentPage * limit
  }, [currentPage])

  const { data } = useQuery({
    queryFn: () => sdk.admin.product.list({
      limit,
      offset,
    }),
    queryKey: [["products", limit, offset]],
  })

  // TODO display table
}

export const config = defineRouteConfig({
  label: "Custom",
  icon: ChatBubbleLeftRight,
})

export default CustomPage
```

In the `CustomPage` component, you define:

- A state variable `currentPage` that stores the current page of the table.
- A `limit` variable, indicating how many items to retrieve per page
- An `offset` memoized variable indicating how many items to skip before the retrieved items. It's calculated as a multiplication of `currentPage` and `limit`.

Then, you use `useQuery` from [Tanstack Query](https://tanstack.com/query/latest) to retrieve products using the JS SDK. You pass `limit` and `offset` as query parameters, and you set the `queryKey`, which is used for caching and revalidation, to be based on the key `products`, along with the current limit and offset. So, whenever the `offset` variable changes, the request is sent again to retrieve the products of the current page.

You can change the query to send a request to a custom API route as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk#send-requests-to-custom-routes/index.html.md).

Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency.

`useQuery` returns an object containing `data`, which holds the response fields including the products and pagination fields.

Then, to display the table, replace the `TODO` with the following:

```tsx
return (
  <SingleColumnLayout>
    <Container>
      <Header title="Products" />
      {data && (
        <Table
          columns={[
            {
              key: "id",
              label: "ID",
            },
            {
              key: "title",
              label: "Title",
            },
          ]}
          data={data.products as any}
          pageSize={data.limit}
          count={data.count}
          currentPage={currentPage}
          setCurrentPage={setCurrentPage}
        />
      )}
    </Container>
  </SingleColumnLayout>
)
```

Aside from the `Table` component, this UI route also uses the [SingleColumnLayout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/layouts/single-column/index.html.md), [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md), and [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component.

If `data` isn't `undefined`, you display the `Table` component passing it the following props:

- `columns`: The columns to show. You only show the product's ID and title.
- `data`: The rows of the table. You pass it the `products` property of `data`.
- `pageSize`: The maximum number of items per page. You pass it the `count` property of `data`.
- `currentPage` and `setCurrentPage`: The current page and the function to change it.

To test it out, log into the Medusa Admin and open `http://localhost:9000/app/custom`. You'll find a table of products with pagination.


# Single Column Layout - Admin Components

In this guide, you'll learn how to create a layout component that matches the Medusa Admin's design conventions for pages with a single column of content.

The Medusa Admin has pages with a single column of content.

This doesn't include the sidebar, only the main content.

![An example of an admin page with a single column](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286605/Medusa%20Resources/single-column.png)

To create a layout that you can use in UI routes to support one column of content, create the component `src/admin/layouts/single-column.tsx` with the following content:

```tsx title="src/admin/layouts/single-column.tsx"
export type SingleColumnLayoutProps = {
  children: React.ReactNode
}

export const SingleColumnLayout = ({ children }: SingleColumnLayoutProps) => {
  return (
    <div className="flex flex-col gap-y-3">
      {children}
    </div>
  )
}
```

The `SingleColumnLayout` accepts the content in the `children` props.

***

## Example

Use the `SingleColumnLayout` component in your UI routes that have a single column. For example:

```tsx title="src/admin/routes/custom/page.tsx" highlights={[["9"]]}
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { Container } from "../../components/container"
import { SingleColumnLayout } from "../../layouts/single-column"
import { Header } from "../../components/header"

const CustomPage = () => {
  return (
    <SingleColumnLayout>
      <Container>
        <Header title="Custom Page" />
      </Container>
    </SingleColumnLayout>
  )
}

export const config = defineRouteConfig({
  label: "Custom",
  icon: ChatBubbleLeftRight,
})

export default CustomPage
```

This UI route also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and a [Header]() custom components.


# Two Column Layout - Admin Components

In this guide, you'll learn how to create a layout component that matches the Medusa Admin's design conventions for pages with two columns of content.

The Medusa Admin has pages with two columns of content.

This doesn't include the sidebar, only the main content.

![An example of an admin page with two columns](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286690/Medusa%20Resources/two-column_sdnkg0.png)

To create a layout that you can use in UI routes to support two columns of content, create the component `src/admin/layouts/two-column.tsx` with the following content:

```tsx title="src/admin/layouts/two-column.tsx"
export type TwoColumnLayoutProps = {
  firstCol: React.ReactNode
  secondCol: React.ReactNode
}

export const TwoColumnLayout = ({ 
  firstCol,
  secondCol,
}: TwoColumnLayoutProps) => {
  return (
    <div className="flex flex-col gap-x-4 gap-y-3 xl:flex-row xl:items-start">
      <div className="flex w-full flex-col gap-y-3">
          {firstCol}
        </div>
        <div className="flex w-full max-w-[100%] flex-col gap-y-3 xl:mt-0 xl:max-w-[440px]">
          {secondCol}
        </div>
    </div>
  )
}
```

The `TwoColumnLayout` accepts two props:

- `firstCol` indicating the content of the first column.
- `secondCol` indicating the content of the second column.

***

## Example

Use the `TwoColumnLayout` component in your UI routes that have a single column. For example:

```tsx title="src/admin/routes/custom/page.tsx" highlights={[["9"]]}
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { Container } from "../../components/container"
import { Header } from "../../components/header"
import { TwoColumnLayout } from "../../layouts/two-column"

const CustomPage = () => {
  return (
    <TwoColumnLayout
      firstCol={
        <Container>
          <Header title="First Column" />
        </Container>
      }
      secondCol={
        <Container>
          <Header title="Second Column" />
        </Container>
      }
    />
  )
}

export const config = defineRouteConfig({
  label: "Custom",
  icon: ChatBubbleLeftRight,
})

export default CustomPage
```

This UI route also uses [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header]() custom components.


# Admin Components & Layouts

In this section of the documentation, you'll learn how to implement common Medusa Admin components and layouts.

These guides are useful to build components and layouts that follow the same design conventions as the Medusa Admin. The components and layouts are built on top of the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md).

Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/index.html.md) for a full list of components.

## Layouts

These layout components allow you to set the layout of your [UI routes](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) similar to the layouts used in the Medusa Admin.

***

## Components

These components allow you to use common Medusa Admin components in your custom UI routes and widgets.


# create Method - Service Factory Reference

This method of a module's service creates one or more records of a data model.

The method's name is of the format `createDataModel`, where `DataModel` is the plural pascal-case name of the data model.

## Create One Record

```ts
const post = await postModuleService.createPosts({
  name: "My Post",
  published_at: new Date(),
  metadata: {
     external_id: "1234",
  },
})
```

If an object is passed to the method, the created record object is returned.

***

## Create Multiple Records

```ts
const posts = await postModuleService.createPosts([
  {
    name: "My Post",
    published_at: new Date(),
  },
  {
    name: "My Other Post",
    published_at: new Date(),
  },
])
```

If an array of objects is passed to the method, an array containing the created records is returned.


# delete Method - Service Factory Reference

This method of a module's service deletes one or more records.

The method's name is `deleteDataModel`, where `DataModel` is the plural pascal-case name of the data model.

## Delete One Record

```ts
await postModuleService.deletePosts("123")
```

To delete one record, pass its ID as a parameter to the method.

***

## Delete Multiple Records

```ts
await postModuleService.deletePosts([
  "123",
  "321",
])
```

To delete multiple records, pass an array of IDs as a parameter to the method.

***

## Delete Records Matching Filters

```ts
await postModuleService.deletePosts({
  name: "My Post",
})
```

To delete records matching a set of filters, pass an object of filters as a parameter to the method.

Refer to the [Filtering](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md) reference for more information on accepted filters and examples.


# list Method - Service Factory Reference

This method of a module's service retrieves a list of records.

The method's name is `listDataModel`, where `DataModel` is the plural pascal-case name of the data model.

## Retrieve List of Records

```ts
const posts = await postModuleService.listPosts()
```

If no parameters are passed, the method returns an array of the first `15` records.

***

## Filter Records

```ts
const posts = await postModuleService.listPosts({
  id: ["123", "321"],
})
```

### Parameters

To retrieve records matching a set of filters, pass an object of the filters as the first parameter.

Refer to the [Filtering](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md) reference for more information on accepted filters and examples.

### Returns

The method returns an array of the first `15` records matching the filters.

***

## Retrieve Relations

This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).

```ts
const posts = await postModuleService.listPosts({}, {
  relations: ["author"],
})
```

### Parameters

To retrieve records with their relations, pass an object with a `relations` property as the second parameter. `relations`'s value is an array of relation names.

### Returns

The method returns an array of the first `15` records matching the filters.

***

## Select Properties

```ts
const posts = await postModuleService.listPosts({}, {
  select: ["id", "name"],
})
```

### Parameters

By default, retrieved records have all their properties. To select specific properties to retrieve, pass a `select` property in the second object parameter.

`select`'s value is an array of property names to retrieve.

### Returns

The method returns an array of the first `15` records matching the filters.

***

## Paginate Records

```ts
const posts = await postModuleService.listPosts({}, {
  take: 20,
  skip: 10,
})
```

### Parameters

To paginate the returned records, the second object parameter accepts the following properties:

- `take`: a number indicating how many records to retrieve. By default, it's `15`.
- `skip`: a number indicating how many records to skip before the retrieved records. By default, it's `0`.

### Returns

The method returns an array of records. The number of records is less than or equal to `take`'s value.

***

## Sort Records

```ts
const posts = await postModuleService.listPosts({}, {
  order: {
    name: "ASC",
  },
})
```

### Parameters

To sort records by one or more properties, pass an `order` property in the second object parameter. Its value is an object whose keys are the property names, and values can either be:

- `ASC` to sort by this property in the ascending order.
- `DESC` to sort by this property in the descending order.

### Returns

The method returns an array of the first `15` records matching the filters.


# listAndCount Method - Service Factory Reference

This method of a module's service retrieves a list of records with the total count.

The method's name is `listAndCountDataModel`, where `DataModel` is the plural pascal-case name of the data model.

## Retrieve List of Records

```ts
const [posts, count] = await postModuleService.listAndCountPosts()
```

If no parameters are passed, the method returns an array with two items:

1. The first is an array of the first `15` records retrieved.
2. The second is the total count of records.

***

## Filter Records

```ts
const [posts, count] = await postModuleService.listAndCountPosts({
  id: ["123", "321"],
})
```

### Parameters

To retrieve records matching a set of filters, pass an object of the filters as the first parameter.

Refer to the [Filtering](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md) reference for more information on accepted filters and examples.

### Returns

The method returns an array with two items:

1. The first is an array of the first `15` records retrieved matching the specified filters.
2. The second is the total count of records matching the specified filters.

***

## Retrieve Relations

This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).

```ts
const [posts, count] = await postModuleService.listAndCountPosts({}, {
  relations: ["author"],
})
```

### Parameters

To retrieve records with their relations, pass an object with a `relations` property as the second parameter. Its value is an array of relation names.

### Returns

The method returns an array with two items:

1. The first is an array of the first `15` records retrieved.
2. The second is the total count of records.

***

## Select Properties

```ts
const [posts, count] = await postModuleService.listAndCountPosts({}, {
  select: ["id", "name"],
})
```

### Parameters

By default, retrieved records have all their properties. To select specific properties to retrieve, pass a `select` property in the second object parameter.

`select`'s value is an array of property names to retrieve.

### Returns

The method returns an array with two items:

1. The first is an array of the first `15` records retrieved.
2. The second is the total count of records.

***

## Paginate Records

```ts
const [posts, count] = await postModuleService.listAndCountPosts({}, {
  take: 20,
  skip: 10,
})
```

### Parameters

To paginate the returned records, the second object parameter accepts the following properties:

- `take`: a number indicating how many records to retrieve. By default, it's `15`.
- `skip`: a number indicating how many records to skip before the retrieved records. By default, it's `0`.

### Returns

The method returns an array with two items:

1. The first is an array of the records retrieved. The number of records is less than or equal to `take`'s value.
2. The second is the total count of records.

***

## Sort Records

```ts
const [posts, count] = await postModuleService.listAndCountPosts({}, {
  order: {
    name: "ASC",
  },
})
```

### Parameters

To sort records by one or more properties, pass an `order` property in the second object parameter. Its value is an object whose keys are the property names, and values can either be:

- `ASC` to sort by this property in the ascending order.
- `DESC` to sort by this property in the descending order.

### Returns

The method returns an array with two items:

1. The first is an array of the first `15` records retrieved.
2. The second is the total count of records.


# restore Method - Service Factory Reference

This method of a module's service restores one or more records of a data model that were [soft-deleted](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/methods/soft-delete/index.html.md).

The method's name is of the format `restoreDataModel`, where `DataModel` is the plural pascal-case name of the data model.

## Restore One Record

```ts
const restoredPosts = await postModuleService.restorePosts("123")
```

### Parameters

To restore one record, pass its ID as a parameter to the method.

### Returns

The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs.

For example, the returned object of the above example is:

```json
{
  "post_id": ["123"],
}
```

***

## Restore Multiple Records

```ts
const restoredPosts = await postModuleService.restorePosts([
  "123",
  "321",
])
```

### Parameters

To restore multiple records, pass an array of IDs as a parameter to the method.

### Returns

The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs.

For example, the returned object of the above example is:

```json
{
  "post_id": [
    "123",
    "321",
  ],
}
```

***

## Restore Records Matching Filters

```ts
const restoredPosts = await postModuleService.restorePosts({
  name: "My Post",
})
```

### Parameters

To restore records matching a set of filters, pass an object of filters as a parameter to the method.

Refer to the [Filtering](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md) reference for more information on accepted filters and examples.

### Returns

The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs.

For example, the returned object of the above example is:

```json
{
  "post_id": [
    "123",
  ],
}
```


# retrieve Method - Service Factory Reference

This method retrieves one record of a data model by its ID.

The method's name is `retrieveDataModel`, where `DataModel` is the singular pascal-case name of the data model.

## Retrieve a Record

```ts
const post = await postModuleService.retrievePost("123")
```

### Parameters

Pass the ID of the record to retrieve.

### Returns

The method returns the record as an object.

***

## Retrieve a Record's Relations

This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).

```ts
const post = await postModuleService.retrievePost("123", {
  relations: ["author"],
})
```

### Parameters

To retrieve the data model with relations, pass an object with the property `relations` as the second parameter. `relations`'s value is an array of relation names.

### Returns

The method returns the record as an object.

***

## Select Properties to Retrieve

```ts
const post = await postModuleService.retrievePost("123", {
  select: ["id", "name"],
})
```

### Parameters

By default, all of the record's properties are retrieved. To select specific ones, pass a `select` property in the second object parameter. Its value is an array of property names.

### Returns

The method returns the record as an object.


# softDelete Method - Service Factory Reference

This method of a module's service soft deletes one or more records of a data model.

The method's name is of the format `softDeleteDataModel`, where `DataModel` is the plural pascal-case name of the data model.

## Soft Delete One Record

```ts
const deletedPosts = await postModuleService.softDeletePosts(
  "123"
)
```

### Parameters

To soft delete a record, pass its ID as a parameter to the method.

### Returns

The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs.

For example, the returned object of the above example is:

```json
{
  "post_id": ["123"]
}
```

***

## Soft Delete Multiple Records

```ts
const deletedPosts = await postModuleService.softDeletePosts([
  "123",
  "321",
])
```

### Parameters

To soft delete multiple records, pass an array of IDs as a parameter to the method.

### Returns

The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs.

For example, the returned object of the above example is:

```json
{
  "post_id": [
    "123",
    "321"
  ]
}
```

***

## Soft Delete Records Matching Filters

```ts
const deletedPosts = await postModuleService.softDeletePosts({
  name: "My Post",
})
```

### Parameters

To soft delete records matching a set of filters, pass an object of filters as a parameter to the method.

Refer to the [Filtering](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md) reference for more information on accepted filters and examples.

### Returns

The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs.

For example, the returned object of the above example is:

```json
{
  "post_id": ["123"]
}
```


# update Method - Service Factory Reference

This method of a module's service updates one or more records.

The method's name is `updateDataModel`, where `DataModel` is the plural pascal-case name of the data model.

## Update One Record

```ts
const post = await postModuleService.updatePosts({
  id: "123",
  name: "My Post",
})
```

### Parameters

To update one record, pass an object that has at least an `id` property, identifying the ID of the record to update.

You can pass in the same object any other properties to update.

### Returns

The method returns the updated record as an object.

***

## Update Multiple Records

```ts
const posts = await postModuleService.updatePosts([
  {
    id: "123",
    name: "My Post",
  },
  {
    id: "321",
    published_at: new Date(),
  },
])
```

### Parameters

To update multiple records, pass an array of objects. Each object must have at least an `id` property, identifying the ID of the record to update.

You can pass in each object any other properties to update.

### Returns

The method returns an array of updated record objects.

***

## Update Records Matching a Filter

```ts
const posts = await postModuleService.updatePosts({
  selector: {
    name: "My Post",
  },
  data: {
    published_at: new Date(),
  },
})
```

### Parameters

To update records that match specified filters, pass an object with two properties as a parameter:

- `selector`: An object of filters that a record must match to be updated.
- `data`: An object of the properties to update in every record that match the filters in `selector`.

In the example above, you update the `published_at` property of every post record whose name is `My Post`.

Refer to the [Filtering](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md) reference for more information on accepted filters and examples.

### Returns

The method returns an array of updated record objects.

***

## Multiple Record Updates with Filters

```ts
const posts = await postModuleService.updatePosts([
  {
    selector: {
      name: "My Post",
    },
    data: {
      published_at: new Date(),
    },
  },
  {
    selector: {
      name: "Another Post",
    },
    data: {
      metadata: {
        external_id: "123",
      },
    },
  },
])
```

### Parameters

To update records matching different sets of filters, pass an array of objects, each having two properties:

- `selector`: An object of filters that a record must match to be updated.
- `data`: An object of the properties to update in every record that match the filters in `selector`.

In the example above, you update the `published_at` property of post records whose name is `My Post`, and update the `metadata` property of post records whose name is `Another Post`.

Refer to the [Filtering](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md) reference for more information on accepted filters and examples.

### Returns

The method returns an array of updated record objects.

***

## Update a JSON Property

```ts highlights={highlights}
const post = await postModuleService.updatePosts({
  id: "123",
  name: "My Post",
  metadata: {
    category: "news",
    tags: ["update", "json"],
  },
})
```

When you have a [JSON property](https://docs.medusajs.com/docs/learn/fundamentals/data-models/json-properties/index.html.md) in your data model, you can update it by adding, updating, or removing properties within that JSON object. Medusa will merge the properties you pass in the `update` method with the existing JSON object.

### Remove a Property from the JSON Property

```ts highlights={highlights}
const post = await postModuleService.updatePosts({
  id: "123",
  name: "My Post",
  metadata: {
    is_featured: "",
  },
})
```

To remove a property from the JSON object, you can set its value to an empty string.

### Learn More about Updating JSON Properties

Refer to the [JSON Properties](https://docs.medusajs.com/docs/learn/fundamentals/data-models/json-properties/index.html.md) documentation to learn more about how JSON properties work in Medusa and how to update them.


# Service Factory Reference

This section of the documentation provides a reference to the methods generated for services extending the service factory (`MedusaService`) and how to use them.

Refer to the [Service Factory](https://docs.medusajs.com/docs/learn/fundamentals/modules/service-factory/index.html.md) documentation to learn more.

## Method Names

When your module's main service extends the service factory, Medusa will:

1. Generate [data-management methods in your main service](#main-service-methods) for each of the data models passed to the `MedusaService` function.
2. Generate [internal services](#internal-generated-service-methods) for each of the data models passed to the `MedusaService` function, which can be resolved in loaders.

### Main Service Methods

The names of the generated methods for a service extending the service factory are of the format `{operationName}{DataModelName}`, where:

- `{operationName}` is the name of the operation. For example, `create`.
- `{DataModelName}` is the pascal-case version of the data model's key that's passed in the object parameter of `MedusaService`. The name is pluralized for all operations except the `retrieve` operation.

Some examples of method names:

- `createPosts` (`Post` data model)
- `createMyPosts` (`MyPost` data model)
- `retrievePost` (`Post` data model)
- `listPosts` (`Post` data model)

### Internal Generated Service Methods

The internal services are useful when you need to perform database operations in loaders, as they're executed before the module's services are registered. Learn more in the [Module Container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) documentation.

For the internal services, the method names are only the operation name, without the data model name.

For example, a `Post` data model would have a `postService` with methods like `create`, `retrieve`, `update`, and `delete`.

***

## Methods Reference

The reference uses only the operation name to refer to the method.


# Filter Records - Service Factory Reference

Many of the service factory's generated methods allow passing filters to perform an operation, such as to update or delete records matching the filters.

This guide provides examples of using filters.

The `list` method is used in the example snippets of this reference, but you can use the same filtering mechanism with any method that accepts filters.

***

## Match Exact Value

```ts
const posts = await postModuleService.listPosts({
  title: "My Post 2",
})
```

If you pass a property with its value, only records whose properties exactly match the value are selected.

In the example above, only posts having the title `My Post 2` are retrieved.

***

## Doesn't Match Exact Value

```ts
const posts = await postModuleService.listPosts({
  title: {
    $ne: "My Post",
  },
})
```

To find records with a property that doesn't match a value, pass an object with a `$ne` property. Its value is the value that a record's property shouldn't match.

In the example above, only posts that don't have the title `My Post` are retrieved.

***

## Match Multiple Values

```ts
const posts = await postModuleService.listPosts({
  views: [
    50,
    100,
  ],
})
```

To find records with a property matching multiple values, pass an array of those values as the property's value in the filter.

In the example above, only posts having either `50` or `100` views are retrieved.

***

## Don't Match Multiple Values

```ts
const posts = await postModuleService.listPosts({
  title: {
    $nin: [
      "My Post",
    ],
  },
})
```

To find records with a property that doesn't match one or more values, pass an object with a `$nin` property. Its value is an array of multiple values that a record's property shouldn't match.

In the example above, only posts that don't have the title `My Post` are retrieved.

***

## Match Text Like Value

This filter only applies to text-like properties, including `text`, `id`, and `enum` properties.

```ts
const posts = await postModuleService.listPosts({
  title: {
    $like: "My%",
  },
})
```

To perform a `like` filter on a record's property, set the property's value to an object with a `$like` property. Its value is the string to use when applying the `like` filter.

The example above matches all posts whose title starts with `My`.

***

## Filter by Null or Not Null

To retrieve records with a property that is `null`, set the property's value to `null`.

For example:

```ts
const posts = await postModuleService.listPosts({
  published_at: null,
})
```

In the example above, only posts that have a `null` publish date are retrieved.

On the other hand, to retrieve records with a property that isn't `null`, set the property's value to an object with a `$ne` property.

For example:

```ts
const posts = await postModuleService.listPosts({
  published_at: {
    $ne: null,
  },
})
```

In the example above, only posts that have a publish date are retrieved.

\--

## Filter by Relation's Property

Consider that your module also has an `Author` data model, and the `Post` data model has a relation to the `Author` data model.

To filter posts by a property of the `Author` data model, you can use nested objects.

For example:

```ts
const posts = await postModuleService.listPosts({
  author: {
    name: "John",
  },
})
```

In the example above, you retrieve posts whose `author` relation has a `name` property equal to `John`.

### Filter by Relation's Property Not Equal

```ts
const posts = await postModuleService.listPosts({
  author: {
    name: {
      $or: [
        {
          name: {
            $eq: null,
          },
        },
        {
          name: {
            $ne: "John",
          },
        },
      ],
    },
  },
})
```

When you need to filter by a relationship property whose value doesn't match a specific condition, you must use an `$or` operator that applies the following conditions:

1. The relationship's property is not set. This is necessary to exclude posts that don't have an author.
2. The relationship's property is not equal to the specific value.

So, in the example above, you retrieve posts either not having an author or having an author whose name is not "John".

***

## Apply Range Filters

This filter only applies to the `number` and `dateTime` properties.

```ts
const posts = await postModuleService.listPosts({
  published_at: {
    $lt: new Date(),
  },
})
```

To filter a record's property to be within a range, set the property's value to an object with any of the following properties:

1. `$lt`: The property's value must be less than the supplied value.
2. `$lte`: The property's value must be less than or equal to the supplied value.
3. `$gt`: The property's value must be greater than the supplied value.
4. `$gte`: The property's value must be greater than or equal the supplied value.

In the example above, only posts whose `published_at` property is before the current date and time are retrieved.

### Example: Retrieve Posts Published Today

```ts
const startToday = new Date()
startToday.setHours(0, 0, 0, 0)

const endToday = new Date()
endToday.setHours(23, 59, 59, 59)

const posts = await postModuleService.listPosts({
  published_at: {
    $gte: startToday,
    $lte: endToday,
  },
})
```

The `dateTime` property also stores the time. So, when matching for an exact day, you must set a range filter to be between the beginning and end of the day.

In this example, you retrieve the current date twice: once to set its time to `00:00:00`, and another to set its time `23:59:59`. Then, you retrieve posts whose `published_at` property is between `00:00:00` and `23:59:59` of today.

### Example: Range Filter on Number Property

```ts
const posts = await postModuleService.listPosts({
  views: {
    $gte: 50,
    $lte: 100,
  },
})
```

In the example above, only posts with `views` between `50` and `100` are retrieved.

***

## Apply Or Condition

```ts
const posts = await postModuleService.listPosts({
  $or: [
    {
      title: "My Post",
    },
    {
      published_at: {
        $lt: new Date(),
      },
    },
  ],
})
```

To use an `or` condition, pass to the filter object the `$or` property, whose value is an array of filters.

In the example above, posts whose title is `My Post` or their `published_at` date is less than the current date and time are retrieved.

***

## Apply And Condition

```ts
const posts = await postModuleService.listPosts({
  $and: [
    {
      title: "My Post",
    },
    {
      published_at: {
        $lt: new Date(),
      },
    },
  ],
})
```

To use an `and` condition, pass to the filter object the `$and` property, whose value is an array of filters.

In the example above, only posts whose title is `My Post` and their `published_at` date is less than the current date and time are retrieved.

***

## Complex Filters Example

```ts
const posts = await postModuleService.listPosts({
  $or: [
    {
      $and: [
        { views: { $gte: 50 } },
        { 
          published_at: {
            $gte: new Date(new Date().getFullYear(), 0, 1),
          },
        },
      ],
    },
    {
      title: {
        $like: "%Featured%",
      },
    },
  ],
})
```

In the example above, posts are retrieved if they meet either of the following conditions:

1. The post has at least `50` views and was published after the beginning of the current year.
2. The post's title contains the word `Featured`.

By combining `and` and `or` conditions, you can create complex filters to retrieve records that meet specific criteria.

***

## Supported Filter Operators List

The following operators are supported by the service factory filtering mechanism:

|Operator|Description|
|---|---|
|Comparison Operators|
|\`$eq\`|Matches values that are equal to a specified value.|
|\`$ne\`|Matches values that are not equal to a specified value.|
|\`$in\`|Matches any of the values specified in an array.|
|\`$nin\`|Matches none of the values specified in an array.|
|\`$like\`|Matches values containing a specified substring.|
|\`$lt\`|Matches values that are less than a specified value.|
|\`$lte\`|Matches values that are less than or equal to a specified value.|
|\`$gt\`|Matches values that are greater than a specified value.|
|\`$gte\`|Matches values that are greater than or equal to a specified value.|
|\`$re\`|Matches values that match a specified regular expression.|
|\`$ilike\`|Matches values containing a specified substring, case-insensitive.|
|\`$fulltext\`|Performs a full-text search on a text property.|
|\`$overlap\`|Matches values that have overlapping elements with the specified array.|
|\`$contains\`|Performs an |
|\`$contained\`|Performs an |
|Logical Operators|
|\`$and\`|Joins two or more conditions with a logical AND.|
|\`$or\`|Joins two or more conditions with a logical OR.|


# Use Stripe's Payment Element in the Next.js Starter Storefront

In this tutorial, you'll learn how to customize the Next.js Starter Storefront to use [Stripe's Payment Element](https://docs.stripe.com/payments/payment-element).

By default, the Next.js Starter Storefront comes with a basic Stripe card payment integration. However, you can replace it with Stripe's Payment Element instead.

By using the Payment Element, you can offer a unified payment experience that supports various payment methods, including credit cards, PayPal, iDeal, and more, all within a single component.

## Summary

By following this tutorial, you'll learn how to:

- Set up a Medusa application with the Stripe Module Provider.
- Customize the Next.js Starter Storefront to use Stripe's Payment Element.

***

## Step 1: Set Up Medusa Project

In this step, you'll set up a Medusa application and configure the Stripe Module Provider. You can skip this step if you already have a Medusa application running with the Stripe Module Provider configured.

### a. Install Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

To install the Medusa application, run the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when you're asked whether you want to install the Next.js Starter Storefront, choose `Y` for yes.

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form.

Afterwards, you can log in with the new user and explore the dashboard. The Next.js Starter Storefront is also running at `http://localhost:8000`.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

### b. Configure Stripe Module Provider

Next, you'll configure the [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) in your Medusa application. The Stripe Module Provider allows you to accept payments through Stripe in your Medusa application.

### Prerequisites

- [Stripe account↗](https://stripe.com)
- [Stripe Secret API Key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard)

The Stripe Module Provider is installed by default in your application. To use it, add it to the array of providers passed to the Payment Module in `medusa-config.ts`:

```ts title="medusa-config.ts" badgeLabel="Medusa Application" badgeColor="green"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/payment",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/payment-stripe",
            id: "stripe",
            options: {
              apiKey: process.env.STRIPE_API_KEY,
            },
          },
        ],
      },
    },
  ],
})
```

For more details about other available options and the webhook URLs that Medusa provides, refer to the [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) documentation.

### c. Set Environment Variables

Next, make sure to add the necessary environment variables for the above options in `.env` in your Medusa application:

```bash badgeLabel="Medusa Application" badgeColor="green"
STRIPE_API_KEY=<YOUR_STRIPE_API_KEY>
```

Where `<YOUR_STRIPE_API_KEY>` is your Stripe [Secret API Key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard).

You also need to add the Stripe [Publishable API Key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard) in the Next.js Starter Storefront's environment variables:

```bash badgeLabel="Storefront" badgeColor="blue"
NEXT_PUBLIC_STRIPE_KEY=<YOUR_STRIPE_PUBLISHABLE_API_KEY>
```

Where `<YOUR_STRIPE_PUBLISHABLE_API_KEY>` is your Stripe [Publishable API Key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard).

### d. Enable Stripe in a Region

Finally, you need to add Stripe as a payment provider to one or more regions in your Medusa Admin. This will allow customers to use Stripe as a payment method during checkout.

To do that:

1. Log in to your Medusa Admin dashboard at `http://localhost:9000/app`.
2. Go to Settings -> Regions.
3. Select a region you want to enable Stripe for (or create a new one).
4. Click the <InlineIcon Icon={EllipsisHorizontal} alt="three-dots" /> icon on the upper right corner.
5. Choose "Edit" from the dropdown menu.
6. In the Payment Providers field, select “Stripe (STRIPE)”
7. Click the Save button.

Do this for all regions you want to enable Stripe for.

***

## Step 2: Update Payment Step in Checkout

You'll start customizing the Next.js Starter Storefront by updating the payment step in the checkout process. By default, the payment step is implemented to show all available payment methods in the region the customer is in, and allows the customer to select one of them.

In this step, you'll replace the current payment method selection with Stripe's Payment Element. You'll no longer need to handle different payment methods separately, as the Payment Element will automatically adapt to the available methods configured in your Stripe account.

### a. Update the Stripe SDKs

To ensure you have the latest Stripe packages, update the `@stripe/react-stripe-js` and `@stripe/stripe-js` packages in your Next.js Starter Storefront:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm install @stripe/react-stripe-js@latest @stripe/stripe-js@latest
```

### b. Update the Payment Component

The `Payment` component in `src/modules/checkout/components/payment/index.tsx` shows the payment step in checkout. It handles the payment method selection and submission. You'll update this component first to use the Payment Element.

The full final code is available at the end of the section.

First, add the following imports at the top of the file:

```tsx title="src/modules/checkout/components/payment/index.tsx"
// ...other imports
import { useContext } from "react"
import { PaymentElement, useElements, useStripe } from "@stripe/react-stripe-js"
import { StripePaymentElementChangeEvent } from "@stripe/stripe-js"
import { StripeContext } from "../payment-wrapper/stripe-wrapper"
```

You import components from the Stripe SDKs, and the `StripeContext` created in the `StripeWrapper` component. This context will allow you to check whether Stripe is ready to be used.

Next, in the `Payment` component, replace the existing state variables with the following:

```tsx title="src/modules/checkout/components/payment/index.tsx"
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [stripeComplete, setStripeComplete] = useState(false)
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>("")

const stripeReady = useContext(StripeContext)
const stripe = stripeReady ? useStripe() : null
const elements = stripeReady ? useElements() : null
```

You define the following variables:

- `isLoading`: A boolean that indicates whether the payment is being processed.
- `error`: A string that holds any error message related to the payment.
- `stripeComplete`: A boolean that indicates whether operations with the Stripe Payment Element are complete. When this is enabled, the customer can proceed to the "Review" checkout step.
- `selectedPaymentMethod`: A string that holds the currently selected payment method in the Payment Element. For example, `card` or `eps`.
- `stripeReady`: A boolean that indicates whether Stripe is ready to be used, fetched from the `StripeContext`.
  - This context is defined in `src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx` and it wraps the checkout page in Stripe's `Elements` component, which initializes the Stripe SDKs.
- `stripe`: The Stripe instance, which is used to interact with the Stripe API. It's only initialized if `stripeReady` from the Stripe context is true.
- `elements`: The Stripe Elements instance, which is used to manage the Payment Element. It's also only initialized if `stripeReady` is true.

Next, add the following function to the `Payment` component to handle changes in the Payment Element:

```tsx title="src/modules/checkout/components/payment/index.tsx"
const handlePaymentElementChange = async (
  event: StripePaymentElementChangeEvent
) => {
  // Catches the selected payment method and sets it to state
  if (event.value.type) {
    setSelectedPaymentMethod(event.value.type)
  }
  
  // Sets stripeComplete on form completion
  setStripeComplete(event.complete)

  // Clears any errors on successful completion
  if (event.complete) {
    setError(null)
  }
}
```

This function will be called on every change in Stripe's Payment Element, such as when the customer selects a payment method.

In the function, you update the `selectedPaymentMethod` state with the type of payment method selected by the customer, set `stripeComplete` to true when the payment element is complete, and clear any error messages when the payment element is complete.

Then, to customize the payment step's submission, replace the `handleSubmit` function with the following:

```tsx title="src/modules/checkout/components/payment/index.tsx"
const handleSubmit = async () => {
  setIsLoading(true)
  setError(null)

  try {
    // Check if the necessary context is ready
    if (!stripe || !elements) {
      setError("Payment processing not ready. Please try again.")
      return
    }

    // Submit the payment method details
    await elements.submit().catch((err) => {
      console.error(err)
      setError(err.message || "An error occurred with the payment")
      return
    })

    // Navigate to the final checkout step
    router.push(pathname + "?" + createQueryString("step", "review"), {
      scroll: false,
    })
  } catch (err: any) {
    setError(err.message)
  } finally {
    setIsLoading(false)
  }
}
```

In this function, you use the `elements.submit()` method to submit the payment method details entered by the customer in the Payment Element. This doesn't actually confirm the payment yet; it just prepares the payment method for confirmation.

Once the payment method is submitted successfully, you navigate the customer to the Review step of the checkout process.

Next, to make sure a payment session is initialized when the customer reaches the payment step, add the following to the `Payment` component:

```tsx title="src/modules/checkout/components/payment/index.tsx"
const initStripe = async () => {
  try {
    await initiatePaymentSession(cart, {
      // TODO: change the provider ID if using a different ID in medusa-config.ts
      provider_id: "pp_stripe_stripe",
    })
  } catch (err) {
    console.error("Failed to initialize Stripe session:", err)
    setError("Failed to initialize payment. Please try again.")
  }
}

useEffect(() => {
  if (!activeSession && isOpen) {
    initStripe()
  }
}, [cart, isOpen, activeSession])
```

You add an `initStripe` function that initiates a payment session in the Medusa server. Notice that you set the provider ID to `pp_stripe_stripe`, which is the ID of the Stripe payment provider in your Medusa application if the ID in `medusa-config.ts` is `stripe`.

If you used a different ID, change the `stripe` in the middle accordingly. For example, if you set the ID to `payment`, you would set the `provider_id` to `pp_payment_stripe`.

You also add a `useEffect` hook that calls `initStripe` when the payment step is opened and there is no active payment session.

You'll now update the component's return statement to render the Payment Element.

First, find the following lines in the return statement:

```tsx title="src/modules/checkout/components/payment/index.tsx"
{!paidByGiftcard && availablePaymentMethods?.length && (
  <>
    <RadioGroup
      value={selectedPaymentMethod}
      onChange={(value: string) => setPaymentMethod(value)}
    >
      {/* ... */}
    </RadioGroup>
  </>
)}
```

And replace them with the following:

```tsx title="src/modules/checkout/components/payment/index.tsx"
{!paidByGiftcard &&
  availablePaymentMethods?.length &&
  stripeReady && (
    <div className="mt-5 transition-all duration-150 ease-in-out">
      <PaymentElement
        onChange={handlePaymentElementChange}
        options={{
          layout: "accordion",
        }}
      />
    </div>
  )
}
```

You replace the radio group with the payment methods available in Medusa with the `PaymentElement` component from the Stripe SDK. You pass it the following props:

- `onChange`: A callback function that is called when the payment element's state changes. You pass the `handlePaymentElementChange` function you defined earlier.
- `options`: An object that contains options for the payment element, such as the `layout`. Refer to [Stripe's documentation](https://docs.stripe.com/payments/payment-element#options) for other options available.

Next, find the button rendered afterward and replace it with the following:

```tsx title="src/modules/checkout/components/payment/index.tsx"
<Button
  size="large"
  className="mt-6"
  onClick={handleSubmit}
  isLoading={isLoading}
  disabled={
    !stripeComplete ||
    !stripe ||
    !elements ||
    (!selectedPaymentMethod && !paidByGiftcard)
  }
  data-testid="submit-payment-button"
>
  Continue to review
</Button>
```

You update the button's `disabled` condition and text.

After that, to ensure the customer can only proceed to the review step if a payment method is selected, find the following lines in the return statement:

```tsx title="src/modules/checkout/components/payment/index.tsx"
{cart && paymentReady && activeSession ? (
  // rest of code...
)}
```

And replace them with the following:

```tsx title="src/modules/checkout/components/payment/index.tsx"
{cart && paymentReady && activeSession && selectedPaymentMethod ? (
  // rest of code...
)}
```

You update the condition to also check if a payment method is selected within the Payment Element.

Finally, find the following lines in the return statement:

```tsx title="src/modules/checkout/components/payment/index.tsx"
<Text>
  {isStripeFunc(selectedPaymentMethod) && cardBrand
    ? cardBrand
    : "Another step will appear"}
</Text>
```

And replace them with the following:

```tsx
<Text>Another step may appear</Text>
```

You show the same text for all payment methods since they're handled by Stripe's Payment Element.

You've now finished customizing the Payment step to show the Stripe Payment Element. This component will show the payment methods configured in your Stripe account.

Feel free to remove unused imports, variables, and functions in the file.

### Full updated code for src/modules/checkout/components/payment/index.tsx

```tsx title="src/modules/checkout/components/payment/index.tsx"
"use client"

import { isStripe as isStripeFunc, paymentInfoMap } from "@lib/constants"
import { initiatePaymentSession } from "@lib/data/cart"
import { CheckCircleSolid, CreditCard } from "@medusajs/icons"
import { Button, Container, Heading, Text, clx } from "@medusajs/ui"
import ErrorMessage from "@modules/checkout/components/error-message"
import Divider from "@modules/common/components/divider"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { useCallback, useContext, useEffect, useState } from "react"
import { PaymentElement, useElements, useStripe } from "@stripe/react-stripe-js"
import { StripePaymentElementChangeEvent } from "@stripe/stripe-js"
import { StripeContext } from "../payment-wrapper/stripe-wrapper"

const Payment = ({
  cart,
  availablePaymentMethods,
}: {
  cart: any
  availablePaymentMethods: any[]
}) => {
  const activeSession = cart.payment_collection?.payment_sessions?.find(
    (paymentSession: any) => paymentSession.status === "pending"
  )
  const stripeReady = useContext(StripeContext)

  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const [stripeComplete, setStripeComplete] = useState(false)
  const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>("")

  const stripe = stripeReady ? useStripe() : null
  const elements = stripeReady ? useElements() : null

  const searchParams = useSearchParams()
  const router = useRouter()
  const pathname = usePathname()

  const isOpen = searchParams.get("step") === "payment"

  const handlePaymentElementChange = async (
    event: StripePaymentElementChangeEvent
  ) => {
    // Catches the selected payment method and sets it to state
    if (event.value.type) {
      setSelectedPaymentMethod(event.value.type)
    }
    
    // Sets stripeComplete on form completion
    setStripeComplete(event.complete)

    // Clears any errors on successful completion
    if (event.complete) {
      setError(null)
    }
  }

  const setPaymentMethod = async (method: string) => {
    setError(null)
    setSelectedPaymentMethod(method)
    if (isStripeFunc(method)) {
      await initiatePaymentSession(cart, {
        provider_id: method,
      })
    }
  }

  const paidByGiftcard =
    cart?.gift_cards && cart?.gift_cards?.length > 0 && cart?.total === 0

  const paymentReady =
    (activeSession && cart?.shipping_methods.length !== 0) || paidByGiftcard

  const createQueryString = useCallback(
    (name: string, value: string) => {
      const params = new URLSearchParams(searchParams)
      params.set(name, value)

      return params.toString()
    },
    [searchParams]
  )

  const handleEdit = () => {
    router.push(pathname + "?" + createQueryString("step", "payment"), {
      scroll: false,
    })
  }

  const handleSubmit = async () => {
    setIsLoading(true)
    setError(null)

    try {
      // Check if the necessary context is ready
      if (!stripe || !elements) {
        setError("Payment processing not ready. Please try again.")
        return
      }

      // Submit the payment method details
      await elements.submit().catch((err) => {
        console.error(err)
        setError(err.message || "An error occurred with the payment")
        return
      })

      // Navigate to the final checkout step
      router.push(pathname + "?" + createQueryString("step", "review"), {
        scroll: false,
      })
    } catch (err: any) {
      setError(err.message)
    } finally {
      setIsLoading(false)
    }
  }

  const initStripe = async () => {
    try {
      await initiatePaymentSession(cart, {
        provider_id: "pp_stripe_stripe",
      })
    } catch (err) {
      console.error("Failed to initialize Stripe session:", err)
      setError("Failed to initialize payment. Please try again.")
    }
  }

  useEffect(() => {
    if (!activeSession && isOpen) {
      initStripe()
    }
  }, [cart, isOpen, activeSession])

  useEffect(() => {
    setError(null)
  }, [isOpen])

  return (
    <div className="bg-white">
      <div className="flex flex-row items-center justify-between mb-6">
        <Heading
          level="h2"
          className={clx(
            "flex flex-row text-3xl-regular gap-x-2 items-baseline",
            {
              "opacity-50 pointer-events-none select-none":
                !isOpen && !paymentReady,
            }
          )}
        >
          Payment
          {!isOpen && paymentReady && <CheckCircleSolid />}
        </Heading>
        {!isOpen && paymentReady && (
          <Text>
            <button
              onClick={handleEdit}
              className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover"
              data-testid="edit-payment-button"
            >
              Edit
            </button>
          </Text>
        )}
      </div>
      <div>
        <div className={isOpen ? "block" : "hidden"}>
          {!paidByGiftcard &&
            availablePaymentMethods?.length &&
            stripeReady && (
              <div className="mt-5 transition-all duration-150 ease-in-out">
                <PaymentElement
                  onChange={handlePaymentElementChange}
                  options={{
                    layout: "accordion",
                  }}
                />
              </div>
            )
          }

          {paidByGiftcard && (
            <div className="flex flex-col w-1/3">
              <Text className="txt-medium-plus text-ui-fg-base mb-1">
                Payment method
              </Text>
              <Text
                className="txt-medium text-ui-fg-subtle"
                data-testid="payment-method-summary"
              >
                Gift card
              </Text>
            </div>
          )}

          <ErrorMessage
            error={error}
            data-testid="payment-method-error-message"
          />

          <Button
            size="large"
            className="mt-6"
            onClick={handleSubmit}
            isLoading={isLoading}
            disabled={
              !stripeComplete ||
              !stripe ||
              !elements ||
              (!selectedPaymentMethod && !paidByGiftcard)
            }
            data-testid="submit-payment-button"
          >
            Continue to review
          </Button>
        </div>

        <div className={isOpen ? "hidden" : "block"}>
          {cart && paymentReady && activeSession && selectedPaymentMethod ? (
            <div className="flex items-start gap-x-1 w-full">
              <div className="flex flex-col w-1/3">
                <Text className="txt-medium-plus text-ui-fg-base mb-1">
                  Payment method
                </Text>
                <Text
                  className="txt-medium text-ui-fg-subtle"
                  data-testid="payment-method-summary"
                >
                  {paymentInfoMap[activeSession?.provider_id]?.title ||
                    activeSession?.provider_id}
                </Text>
              </div>
              <div className="flex flex-col w-1/3">
                <Text className="txt-medium-plus text-ui-fg-base mb-1">
                  Payment details
                </Text>
                <div
                  className="flex gap-2 txt-medium text-ui-fg-subtle items-center"
                  data-testid="payment-details-summary"
                >
                  <Container className="flex items-center h-7 w-fit p-2 bg-ui-button-neutral-hover">
                    {paymentInfoMap[selectedPaymentMethod]?.icon || (
                      <CreditCard />
                    )}
                  </Container>
                  <Text>Another step may appear</Text>
                </div>
              </div>
            </div>
          ) : paidByGiftcard ? (
            <div className="flex flex-col w-1/3">
              <Text className="txt-medium-plus text-ui-fg-base mb-1">
                Payment method
              </Text>
              <Text
                className="txt-medium text-ui-fg-subtle"
                data-testid="payment-method-summary"
              >
                Gift card
              </Text>
            </div>
          ) : null}
        </div>
      </div>
      <Divider className="mt-8" />
    </div>
  )
}

export default Payment
```

### c. Add Icons and Titles for Payment Methods

After a customer enters their payment details and proceeds to the Review step, the payment method is displayed in the collapsed Payment step.

To ensure the correct icon and title are shown for your payment methods configured through Stripe, you can add them in the `paymentInfoMap` object defined in `src/lib/constants.tsx`.

For example:

```tsx title="src/lib/constants.tsx"
export const paymentInfoMap: Record<
  string,
  { title: string; icon: React.JSX.Element }
> = {
  // ...
  card: {
    title: "Credit card",
    icon: <CreditCard />,
  },
  paypal: {
    title: "PayPal",
    icon: <PayPal />,
  },
}
```

For every payment method you want to customize its display, add an entry in the `paymentInfoMap` object. The key should match the [type enum in Stripe's Payment Element](https://docs.stripe.com/api/payment_methods/object#payment_method_object-type), and the value is an object with the following properties:

- `title`: The title to display for the payment method.
- `icon`: A JSX element representing the icon for the payment method. You can use icons from [Medusa UI](https://docs.medusajs.com/ui/icons/overview/index.html.md) or custom icons.

### Test it out

Before you test out the payment checkout step, make sure you have the necessary [payment methods configured in Stripe](https://docs.stripe.com/payments/payment-methods/overview).

Then, follow these steps to test the payment checkout step in your Next.js Starter Storefront:

1. Start the Medusa application with the following command:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

2. Start the Next.js Starter Storefront with the following command:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

3. Open the storefront at `http://localhost:8000` in your browser.
4. Go to Menu -> Store, choose a product, and add it to the cart.
5. Proceed to checkout by clicking the cart icon in the top right corner, then click "Go to checkout".
6. Complete the Address and Delivery steps that are before the Payment step.
7. Once you reach the Payment step, your Stripe Payment Element should appear and list the different payment methods you’ve enabled in your Stripe account.

At this point, you can proceed to the Review step, but you can't confirm the payment with Stripe and place an order in Medusa. You'll customize the payment button in the next step to handle the payment confirmation with Stripe.

![The Stripe Payment Element allows you to pay with different payment methods like PayPal](https://res.cloudinary.com/dza7lstvk/image/upload/v1734005395/Medusa%20Resources/Screenshot_2024-12-10_at_18.04.52_wemqwg.jpg)

***

## Step 3: Update the Payment Button

Next, you'll update the payment button in the Review step of the checkout process. This button is used to confirm the payment with Stripe, then place the order in Medusa.

In this step, you'll customize the `PaymentButton` component in `src/modules/checkout/components/payment-button/index.tsx` to support confirming the payment with Stripe's Payment Element.

The full final code is available at the end of the section.

Start by adding the following imports at the top of the file:

```tsx title="src/modules/checkout/components/payment-button/index.tsx"
// ...other imports
import { useEffect } from "react"
import { useParams, usePathname, useRouter } from "next/navigation"
```

Then, in the `StripePaymentButton` component, add the following variables:

```tsx title="src/modules/checkout/components/payment-button/index.tsx"
const { countryCode } = useParams()
const router = useRouter()
const pathname = usePathname()
const paymentSession = cart.payment_collection?.payment_sessions?.find(
  // TODO change the provider_id if using a different ID in medusa-config.ts
  (session) => session.provider_id === "pp_stripe_stripe"
)
```

You define the following variables:

- `countryCode`: The country code of the customer's region, fetched from the URL. You'll use this later to create Stripe's redirect URL.
- `router`: The Next.js router instance. You'll use this to redirect back to the payment step if necessary.
- `pathname`: The current pathname. You'll use this when redirecting back to the payment step if necessary.
- `paymentSession`: The Medusa payment session for the Stripe payment provider. This is used to get the `clientSecret` needed to confirm the payment.
  - Notice that the provider ID is set to `pp_stripe_stripe`, which is the ID of the Stripe payment provider in your Medusa application if the ID in `medusa-config.ts` is `stripe`. If you used a different ID, change the `stripe` in the middle accordingly.

After that, change the `handlePayment` function to the following:

```tsx title="src/modules/checkout/components/payment-button/index.tsx"
const handlePayment = async () => {
  if (!stripe || !elements || !cart) {
    return
  }
  setSubmitting(true)

  const { error: submitError } = await elements.submit()
  if (submitError) {
    setErrorMessage(submitError.message || null)
    setSubmitting(false)
    return
  }

  const clientSecret = paymentSession?.data?.client_secret as string

  await stripe
  .confirmPayment({
    elements,
    clientSecret,
    confirmParams: {
      return_url: `${
        window.location.origin
      }/api/capture-payment/${cart.id}?country_code=${countryCode}`,
      payment_method_data: {
        billing_details: {
          name:
            cart.billing_address?.first_name +
            " " +
            cart.billing_address?.last_name,
          address: {
            city: cart.billing_address?.city ?? undefined,
            country: cart.billing_address?.country_code ?? undefined,
            line1: cart.billing_address?.address_1 ?? undefined,
            line2: cart.billing_address?.address_2 ?? undefined,
            postal_code: cart.billing_address?.postal_code ?? undefined,
            state: cart.billing_address?.province ?? undefined,
          },
          email: cart.email,
          phone: cart.billing_address?.phone ?? undefined,
        },
      },
    },
    redirect: "if_required",
  })
  .then(({ error, paymentIntent }) => {
    if (error) {
      const pi = error.payment_intent

      if (
        (pi && pi.status === "requires_capture") ||
        (pi && pi.status === "succeeded")
      ) {
        onPaymentCompleted()
        return
      }

      setErrorMessage(error.message || null)
      setSubmitting(false)
      return
    }

    if (
      paymentIntent.status === "requires_capture" ||
      paymentIntent.status === "succeeded"
    ) {
      onPaymentCompleted()
    }
  })
}
```

In the function, you:

- Ensure that the `stripe`, `elements`, and `cart` are available before proceeding.
- Set the `submitting` state to true to indicate that the payment is being processed.
- Use `elements.submit()` to submit the payment method details that the customer enters in the Payment Element. This ensures all necessary payment details are entered.
- Use `stripe.confirmPayment()` to confirm the payment with Stripe. You pass it the following details:
  - `elements`: The Stripe Elements instance that contains the Payment Element.
  - `clientSecret`: The client secret from the payment session, which is used to confirm the payment.
  - `confirmParams`: An object that contains the parameters for confirming the payment.
    - `return_url`: The URL to redirect the customer to after the payment is confirmed. This redirect URL is useful when using providers like PayPal, where the customer is redirected to complete the payment externally. You'll create the route in your Next.js Starter Storefront in the next step.
    - `payment_method_data`: An object that contains the billing details of the customer.
  - `redirect`: The redirect behavior for the payment confirmation. By setting `redirect: "if_required"`, you'll only redirect to the `return_url` if the payment is completed externally.
- Handle the response from `confirmPayment()`.
  - If the payment is successful or requires capture, you call the `onPaymentCompleted` function to complete the payment and place the order.
  - If an error occurred, you set the `errorMessage` state variable to show the error to the customer.

Finally, add the following `useEffect` hooks to the `StripePaymentButton` component:

```tsx title="src/modules/checkout/components/payment-button/index.tsx"
useEffect(() => {
  if (cart.payment_collection?.status === "authorized") {
    onPaymentCompleted()
  }
}, [cart.payment_collection?.status])

useEffect(() => {
  elements?.getElement("payment")?.on("change", (e) => {
    if (!e.complete) {
      // redirect to payment step if not complete
      router.push(pathname + "?step=payment", {
        scroll: false,
      })
    }
  })
}, [elements])
```

You add two effects:

- An effect that runs when the status of the cart's payment collection changes. It triggers the `onPaymentCompleted` function to finalize the order placement when the payment collection status is `authorized`.
- An effect that listens for changes in the Payment Element. If the payment element is not complete, it redirects the customer back to the payment step. This is useful if the customer refreshes the page or navigates away, leading to incomplete payment details.

You've finalized changes to the `PaymentButton` component. Feel free to remove unused imports, variables, and functions in the file.

You still need to add the redirect URL route before you can test the payment button. You'll do that in the next step.

### Full updated code for src/modules/checkout/components/payment-button/index.tsx

```tsx title="src/modules/checkout/components/payment-button/index.tsx"
"use client"

import { isManual, isStripe } from "@lib/constants"
import { placeOrder } from "@lib/data/cart"
import { HttpTypes } from "@medusajs/types"
import { Button } from "@medusajs/ui"
import { useElements, useStripe } from "@stripe/react-stripe-js"
import React, { useState, useEffect } from "react"
import ErrorMessage from "../error-message"
import { useParams, usePathname, useRouter } from "next/navigation"


type PaymentButtonProps = {
  cart: HttpTypes.StoreCart
  "data-testid": string
}

const PaymentButton: React.FC<PaymentButtonProps> = ({
  cart,
  "data-testid": dataTestId,
}) => {
  const notReady =
    !cart ||
    !cart.shipping_address ||
    !cart.billing_address ||
    !cart.email ||
    (cart.shipping_methods?.length ?? 0) < 1

  const paymentSession = cart.payment_collection?.payment_sessions?.[0]

  switch (true) {
    case isStripe(paymentSession?.provider_id):
      return (
        <StripePaymentButton
          notReady={notReady}
          cart={cart}
          data-testid={dataTestId}
        />
      )
    case isManual(paymentSession?.provider_id):
      return (
        <ManualTestPaymentButton notReady={notReady} data-testid={dataTestId} />
      )
    default:
      return <Button disabled>Select a payment method</Button>
  }
}

const StripePaymentButton = ({
  cart,
  notReady,
  "data-testid": dataTestId,
}: {
  cart: HttpTypes.StoreCart
  notReady: boolean
  "data-testid"?: string
}) => {
  const [submitting, setSubmitting] = useState(false)
  const [errorMessage, setErrorMessage] = useState<string | null>(null)

  const { countryCode } = useParams()
  const router = useRouter()
  const pathname = usePathname()
  const paymentSession = cart.payment_collection?.payment_sessions?.find(
    (session) => session.provider_id === "pp_stripe_stripe"
  )

  const onPaymentCompleted = async () => {
    await placeOrder()
      .catch((err) => {
        setErrorMessage(err.message)
      })
      .finally(() => {
        setSubmitting(false)
      })
  }

  const stripe = useStripe()
  const elements = useElements()

  const disabled = !stripe || !elements ? true : false

  const handlePayment = async () => {
    if (!stripe || !elements || !cart) {
      return
    }
    
    setSubmitting(true)


    const { error: submitError } = await elements.submit()
    if (submitError) {
      setErrorMessage(submitError.message || null)
      setSubmitting(false)
      return
    }

    const clientSecret = paymentSession?.data?.client_secret as string

    await stripe
    .confirmPayment({
      elements,
      clientSecret,
      confirmParams: {
        return_url: `${window.location.origin}/api/capture-payment/${cart.id}?country_code=${countryCode}`,
        payment_method_data: {
          billing_details: {
            name:
              cart.billing_address?.first_name +
              " " +
              cart.billing_address?.last_name,
            address: {
              city: cart.billing_address?.city ?? undefined,
              country: cart.billing_address?.country_code ?? undefined,
              line1: cart.billing_address?.address_1 ?? undefined,
              line2: cart.billing_address?.address_2 ?? undefined,
              postal_code: cart.billing_address?.postal_code ?? undefined,
              state: cart.billing_address?.province ?? undefined,
            },
            email: cart.email,
            phone: cart.billing_address?.phone ?? undefined,
          },
        },
      },
      redirect: "if_required",
    })
    .then(({ error, paymentIntent }) => {
      if (error) {
        const pi = error.payment_intent

        if (
          (pi && pi.status === "requires_capture") ||
          (pi && pi.status === "succeeded")
        ) {
          onPaymentCompleted()
          return
        }

        setErrorMessage(error.message || null)
        setSubmitting(false)
        return
      }

      if (
        paymentIntent.status === "requires_capture" ||
        paymentIntent.status === "succeeded"
      ) {
        onPaymentCompleted()
      }
    })
  }

  useEffect(() => {
    if (cart.payment_collection?.status === "authorized") {
      onPaymentCompleted()
    }
  }, [cart.payment_collection?.status])

  useEffect(() => {
    elements?.getElement("payment")?.on("change", (e) => {
      if (!e.complete) {
        // redirect to payment step if not complete
        router.push(pathname + "?step=payment", {
          scroll: false,
        })
      }
    })
  }, [elements])

  return (
    <>
      <Button
        disabled={disabled || notReady}
        onClick={handlePayment}
        size="large"
        isLoading={submitting}
        data-testid={dataTestId}
      >
        Place order
      </Button>
      <ErrorMessage
        error={errorMessage}
        data-testid="stripe-payment-error-message"
      />
    </>
  )
}

const ManualTestPaymentButton = ({ notReady }: { notReady: boolean }) => {
  const [submitting, setSubmitting] = useState(false)
  const [errorMessage, setErrorMessage] = useState<string | null>(null)

  const onPaymentCompleted = async () => {
    await placeOrder()
      .catch((err) => {
        setErrorMessage(err.message)
      })
      .finally(() => {
        setSubmitting(false)
      })
  }

  const handlePayment = () => {
    setSubmitting(true)

    onPaymentCompleted()
  }

  return (
    <>
      <Button
        disabled={notReady}
        isLoading={submitting}
        onClick={handlePayment}
        size="large"
        data-testid="submit-order-button"
      >
        Place order
      </Button>
      <ErrorMessage
        error={errorMessage}
        data-testid="manual-payment-error-message"
      />
    </>
  )
}

export default PaymentButton
```

***

## Step 4: Handle External Payment Callbacks

Some payment providers, such as PayPal, require the customers to perform actions on their portal before authorizing or confirming the payment. In those scenarios, you need an endpoint that the provider redirects the customer to complete their purchase.

In this step, you'll create an API route in your Next.js Starter Storefront that handles this use case. This route is the `return_url` route you passed to the `stripe.confirmPayment` earlier.

Create the file `src/app/api/capture-payment/[cartId]/route.ts` with the following content:

```tsx title="src/app/api/capture-payment/[cartId]/route.ts"
import { placeOrder, retrieveCart } from "@lib/data/cart"
import { NextRequest, NextResponse } from "next/server"

type Params = Promise<{ cartId: string }>

export async function GET(req: NextRequest, { params }: { params: Params }) {
  const { cartId } = await params
  const { origin, searchParams } = req.nextUrl

  const paymentIntent = searchParams.get("payment_intent")
  const paymentIntentClientSecret = searchParams.get(
    "payment_intent_client_secret"
  )
  const redirectStatus = searchParams.get("redirect_status") || ""
  const countryCode = searchParams.get("country_code")

  const cart = await retrieveCart(cartId)

  if (!cart) {
    return NextResponse.redirect(`${origin}/${countryCode}`)
  }

  const paymentSession = cart.payment_collection?.payment_sessions?.find(
    (payment) => payment.data.id === paymentIntent
  )

  if (
    !paymentSession ||
    paymentSession.data.client_secret !== paymentIntentClientSecret ||
    !["pending", "succeeded"].includes(redirectStatus) ||
    !["pending", "authorized"].includes(paymentSession.status)
  ) {
    return NextResponse.redirect(
      `${origin}/${countryCode}/cart?step=review&error=payment_failed`
    )
  }

  const order = await placeOrder(cartId)

  return NextResponse.redirect(
    `${origin}/${countryCode}/order/${order.id}/confirmed`
  )
}
```

In this route, you validate that the payment intent and client secret match the cart's payment session data. If so, you place the order and redirect the customer to the order confirmation page.

If the payment failed or other validation checks fail, you redirect the customer back to the cart page with an error message.

Stripe will redirect the customer back to this route when using payment methods like PayPal.

***

### Test Payment Confirmation and Order placement

You can now test out the entire checkout flow with payment confirmation and order placement.

Start the Medusa application and the Next.js Starter Storefront as you did in the previous steps, and proceed to checkout.

In the payment step, select a payment method, such as card or PayPal. Fill out the necessary details in the Payment Element, then click the "Continue to review" button.

Find test cards in [Stripe's documentation](https://docs.stripe.com/testing#use-test-cards).

After that, click the "Place order" button. If you selected a payment method that requires external confirmation, such as PayPal, you will be redirected to the PayPal payment page.

![Example of handling payment through PayPal](https://res.cloudinary.com/dza7lstvk/image/upload/v1734006541/Medusa%20Resources/Screenshot_2024-12-10_at_19.27.13_q0w6u1.jpg)

Once the payment is confirmed and the order is placed, the customer will be redirected to the order confirmation page, where they can see the order details.

### Test 3D Secure Payments

Stripe's Payment Element also supports 3D Secure payments. You can use one of [Stripe's test 3D Secure cards](https://docs.stripe.com/testing?testing-method=card-numbers#regulatory-cards) to test this feature.

When a customer uses a 3D Secure card, a pop-up will open prompting them to complete the authentication process. If successful, the order will be placed, and the customer will be redirected to the order confirmation page.

![Stripe 3D Secure pop-up](https://res.cloudinary.com/dza7lstvk/image/upload/v1752577384/Medusa%20Resources/CleanShot_2025-07-15_at_11.12.44_2x_ibjxvx.png)

### Server Webhook Verification

Webhook verification is useful to ensure that payment events are handled despite connection issues. The Stripe Module Provider in Medusa provides webhook verification out of the box, so you don't need to implement it yourself.

Learn more about the webhook API routes and how to configure them in the [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) guide.

### Testing Declined Payments

You can use [Stripe's declined test cards](https://docs.stripe.com/testing#declined-payments) to test declined payments. When a payment is declined, the customer will be redirected back to the payment step to fix their payment details as necessary.


# Remove Country Code Prefix in Next.js Starter Storefront

In this guide, you'll learn how to remove the country code prefix from the URLs in the Next.js Starter Storefront.

## Overview

By default, the Next.js Starter Storefront includes a country code prefix in URLs that indicates the customer's selected country. For example, if a customer selects the United States, the URL includes the prefix `/us`.

You may want to remove the country code prefix for a cleaner URL structure or to handle country selection differently.

### Summary

To remove the country code prefix from URLs, you need an alternative way to store and access the country code. The country code is necessary to determine the associated region, which is used for carts, prices, and available payment methods.

This guide shows you how to:

1. Store and manage the country code using cookies.
2. Update the middleware to handle country code retrieval from cookies.
3. Restructure the routes to remove the country code prefix.
4. Update components and pages to use the country code from cookies.

***

## 1. Add Country Code Cookie Utilities

You'll start by adding utility functions to manage the country code cookie.

In `src/lib/data/cookies.ts`, add the following at the end of the file:

```ts title="src/lib/data/cookies.ts"
export const COUNTRY_CODE_COOKIE_NAME = "_medusa_country_code"

/**
 - Gets the current country code from cookies
 */
export const getCountryCode = async (): Promise<string | null> => {
  try {
    const cookies = await nextCookies()
    return cookies.get(COUNTRY_CODE_COOKIE_NAME)?.value ?? null
  } catch {
    return null
  }
}

/**
 - Sets the country code cookie
 */
export const setCountryCode = async (countryCode: string) => {
  const cookies = await nextCookies()
  cookies.set(COUNTRY_CODE_COOKIE_NAME, countryCode, {
    maxAge: 60 * 60 * 24 * 365, // 1 year
    httpOnly: false, // Allow client-side access
    sameSite: "strict",
    secure: process.env.NODE_ENV === "production",
  })
}
```

You add two utility functions:

1. `getCountryCode`: Retrieves the country code from cookies.
2. `setCountryCode`: Sets the country code cookie with appropriate options.

The country code is stored in the `_medusa_country_code` cookie, following the Next.js Starter Storefront's naming conventions.

***

## 2. Update Middleware to Remove Country Code from URL

The Next.js Starter Storefront uses middleware at `src/middleware.ts` to handle country code prefixes in URLs, among other functionalities.

You'll update the middleware to remove the country code prefix from URLs and store it in a cookie instead. This ensures that all requests are handled correctly without requiring a country code in the URL.

In `src/middleware.ts`, add the following import at the top of the file:

```ts title="src/middleware.ts"
import { COUNTRY_CODE_COOKIE_NAME } from "@lib/data/cookies"
```

Then, update the `getCountryCode` function in the middleware to the following:

```ts title="src/middleware.ts"
/**
 - Determines the country code from cookie or headers.
 - @param request
 - @param regionMap
 */
async function getCountryCode(
  request: NextRequest,
  regionMap: Map<string, HttpTypes.StoreRegion | number>
) {
  try {
    // First, check if country code is already in cookie
    const cookieCountryCode = request.cookies.get(COUNTRY_CODE_COOKIE_NAME)?.value?.toLowerCase()
    if (cookieCountryCode && regionMap.has(cookieCountryCode)) {
      return cookieCountryCode
    }

    // Check Vercel IP country header
    const vercelCountryCode = request.headers
      .get("x-vercel-ip-country")
      ?.toLowerCase()
    if (vercelCountryCode && regionMap.has(vercelCountryCode)) {
      return vercelCountryCode
    }

    // Fall back to default region
    if (regionMap.has(DEFAULT_REGION)) {
      return DEFAULT_REGION
    }

    // Last resort: use first available region
    if (regionMap.keys().next().value) {
      return regionMap.keys().next().value
    }

    return null
  } catch (error) {
    if (process.env.NODE_ENV === "development") {
      console.error(
        "Middleware.ts: Error getting the country code. Did you set up regions in your Medusa Admin and define a MEDUSA_BACKEND_URL environment variable? Note that the variable is no longer named NEXT_PUBLIC_MEDUSA_BACKEND_URL."
      )
    }
    return null
  }
}
```

In this updated function, you:

1. Check if the country code is already stored in the cookie.
2. Fall back to the Vercel IP country header if not found in the cookie.
3. Use the default region or the first available region if necessary.

Finally, update the `middleware` function to the following:

```ts title="src/middleware.ts"
/**
 - Middleware to handle region selection and country code cookie management.
 */
export async function middleware(request: NextRequest) {
  // Check if the url is a static asset
  if (request.nextUrl.pathname.includes(".")) {
    return NextResponse.next()
  }

  const cacheIdCookie = request.cookies.get("_medusa_cache_id")
  const cacheId = cacheIdCookie?.value || crypto.randomUUID()

  const regionMap = await getRegionMap(cacheId)

  if (!regionMap) {
    return new NextResponse(
      "No valid regions configured. Please set up regions with countries in your Medusa Admin.",
      { status: 500 }
    )
  }

  const countryCode = await getCountryCode(request, regionMap)

  if (!countryCode) {
    return new NextResponse(
      "No valid regions configured. Please set up regions with countries in your Medusa Admin.",
      { status: 500 }
    )
  }

  // Create response
  const response = NextResponse.next()

  // Set cache ID cookie if not set
  if (!cacheIdCookie) {
    response.cookies.set("_medusa_cache_id", cacheId, {
      maxAge: 60 * 60 * 24,
    })
  }

  // Set country code cookie if not set or different
  const cookieCountryCode = request.cookies.get(COUNTRY_CODE_COOKIE_NAME)?.value
  if (!cookieCountryCode || cookieCountryCode !== countryCode) {
    response.cookies.set(COUNTRY_CODE_COOKIE_NAME, countryCode, {
      maxAge: 60 * 60 * 24 * 365, // 1 year
      httpOnly: false, // Allow client-side access
      sameSite: "strict",
      secure: process.env.NODE_ENV === "production",
    })
  }

  return response
}
```

In this updated `middleware` function, you make the following key changes regarding the country code:

1. Retrieve the country code using the updated `getCountryCode` function.
2. Set the country code cookie if it's not already set or if it differs from the current value.

With these changes, the middleware no longer requires the country code prefix in URLs and manages the country code using cookies instead.

***

## 3. Remove Routes from Country Code Prefix

Next, you need to restructure the routes in the Next.js Starter Storefront to remove the country code prefix.

Currently, there are two directories under `src/app/[countryCode]`: `(checkout)` and `(main)`. These directories contain all routes that include the country code prefix.

To remove the country code prefix, move the `(checkout)` and `(main)` directories directly under `src/app`, then delete the `[countryCode]` directory. Make sure to update any imports or references to and from the routes in these directories accordingly.

![Diagram illustrating the directory structure before and after removing the country code prefix.](https://res.cloudinary.com/dza7lstvk/image/upload/v1767091142/Medusa%20Resources/nextjs-country-code-structure_wpm0yp.jpg)

***

## 4. Update Country Selector to Use Cookie

Next, you'll update the `CountrySelect` component that allows customers to select their country from the side menu. You'll modify it to set the country code cookie when a customer selects a different country, rather than changing the URL.

In `src/modules/layout/components/country-select/index.tsx`, add the following helper function before the `CountrySelect` component:

```ts title="src/modules/layout/components/country-select/index.tsx"
// Helper function to get cookie value on client side
function getCookie(name: string): string | null {
  if (typeof document === "undefined") {return null}
  const value = `; ${document.cookie}`
  const parts = value.split(`; ${name}=`)
  if (parts.length === 2) {return parts.pop()?.split(";").shift() || null}
  return null
}
```

This function retrieves cookie values on the client side, allowing you to update the selected country in the dropdown based on the cookie value without refreshing the page.

Then, inside the `CountrySelect` component, add a new variable and update the `currentPath` variable declaration:

```ts title="src/modules/layout/components/country-select/index.tsx"
const CountrySelect = ({ toggleState, regions }: CountrySelectProps) => {
  const [countryCode, setCountryCode] = useState<string | null>(null)
  const currentPath = usePathname()
  // ...
}
```

You define a new state variable `countryCode` to store the currently selected country code from the cookie. You also update the `currentPath` variable declaration to remove the country code retrieval from the URL.

Next, add the following functions in the `CountrySelect` component to handle country selection and changes:

```ts title="src/modules/layout/components/country-select/index.tsx"
const CountrySelect = ({ toggleState, regions }: CountrySelectProps) => {
  // ...

  // Function to update country code from cookie
  const updateCountryCodeFromCookie = () => {
    const cookieCountryCode = getCookie("_medusa_country_code")
    setCountryCode(cookieCountryCode)
  }

  useEffect(() => {
    // Get country code from cookie on client side
    updateCountryCodeFromCookie()

    // Listen for focus events to refresh country code when user returns to the page
    const handleFocus = () => {
      updateCountryCodeFromCookie()
    }

    window.addEventListener("focus", handleFocus)
    return () => window.removeEventListener("focus", handleFocus)
  }, [])

  const handleChange = async (option: CountryOption) => {
    // Optimistically update the UI immediately
    const newCountryCode = option.country.toLowerCase()
    setCountryCode(newCountryCode)
    const selectedOption = options?.find(
      (o) => o?.country?.toLowerCase() === newCountryCode
    )
    if (selectedOption && selectedOption.country) {
      setCurrent({
        country: selectedOption.country,
        region: selectedOption.region,
        label: selectedOption.label,
      })
    }
    close()

    try {
      // Update the region (this will set the cookie and redirect)
      await updateRegion(option.country, currentPath)
    } catch (error) {
      // If update fails, revert to previous country code
      updateCountryCodeFromCookie()
      console.error("Failed to update region:", error)
    }
  }

  // ...
}
```

You add two functions:

1. `updateCountryCodeFromCookie`: Retrieves the country code from the cookie and updates the state.
2. `handleChange`: Handles country selection changes, optimistically updates the UI, sets the country code cookie, and reverts the UI if the update fails.

Then, update the `useEffect` usage in the component to the following:

```ts title="src/modules/layout/components/country-select/index.tsx"
const CountrySelect = ({ toggleState, regions }: CountrySelectProps) => {
  // ...

  useEffect(() => {
    if (countryCode) {
      const option = options?.find(
        (o) => o?.country === countryCode.toLowerCase()
      )
      setCurrent(option)
    }
  }, [options, countryCode])

  // ...
}
```

You make a small change to transform the country code to lowercase when finding the corresponding option.

Finally, in the return statement of the `CountrySelect` component, update the `defaultValue` prop of the `Listbox` component to the following:

```tsx title="src/modules/layout/components/country-select/index.tsx"
const CountrySelect = ({ toggleState, regions }: CountrySelectProps) => {
  // ...
  return (
    <div>
      <Listbox
        as="span"
        onChange={handleChange}
        defaultValue={
          countryCode
            ? (options?.find(
              (o) => o?.country?.toLowerCase() === countryCode.toLowerCase()
            ) as CountryOption | undefined)
            : undefined
        }
      >
      {/* ... */}
      </Listbox>
    </div>
  )
}
```

You update the `defaultValue` prop to find the option based on the country code stored in the cookie.

With these changes, the `CountrySelect` component now uses cookies to set and retrieve the country code instead of the URL.

***

## 5. Update Country Code Retrieval in Pages

Next, you'll update how the country code is retrieved across various pages of the Next.js Starter Storefront.

The paths of the files mentioned in this section follow the new structure without the `[countryCode]` directory, as described in [Step 3](#3-remove-routes-from-country-code-prefix).

### a. Update Home Page

In `src/app/(main)/page.tsx`, add the following import at the top of the file:

```ts title="src/app/(main)/page.tsx"
import { getCountryCode } from "@lib/data/cookies"
```

Then, change the `Home` component to remove the `countryCode` parameter and retrieve the country code using the `getCountryCode` utility:

```ts title="src/app/(main)/page.tsx"
export default async function Home() {
  const countryCode = await getCountryCode()

  if (!countryCode) {
    return null
  }

  // ...
}
```

### b. Update Store Page

In `src/app/(main)/store/page.tsx`, add the following import at the top of the file:

```tsx title="src/app/(main)/store/page.tsx"
import { getCountryCode } from "@lib/data/cookies"
```

Then, remove the `params` prop from the `Params` type. It should only have a `searchParams` prop:

```tsx title="src/app/(main)/store/page.tsx"
type Params = {
  searchParams: Promise<{
    sortBy?: SortOptions
    page?: string
  }>
}
```

Finally, update the `StorePage` component to retrieve the country code using the `getCountryCode` utility:

```tsx title="src/app/(main)/store/page.tsx"
export default async function StorePage(props: Params) {
  const searchParams = await props.searchParams
  const { sortBy, page } = searchParams
  const countryCode = await getCountryCode()

  if (!countryCode) {
    return null
  }

  return (
    <StoreTemplate
      sortBy={sortBy}
      page={page}
      countryCode={countryCode}
    />
  )
}
```

### c. Update Product Page

In `src/app/(main)/products/[handle]/page.tsx`, add the following import at the top of the file:

```tsx title="src/app/(main)/products/[handle]/page.tsx"
import { getCountryCode } from "@lib/data/cookies"
```

Then, update the `Props` type to remove `countryCode` from the `params` prop:

```tsx title="src/app/(main)/products/[handle]/page.tsx"
type Props = {
  params: Promise<{ handle: string }>
  searchParams: Promise<{ v_id?: string }>
}
```

Next, update the `generateMetadata` function to retrieve the country code using the `getCountryCode` utility:

```tsx title="src/app/(main)/products/[handle]/page.tsx"
export async function generateMetadata(props: Props): Promise<Metadata> {
  const countryCode = await getCountryCode()

  if (!countryCode) {
    notFound()
  }

  const region = await getRegion(countryCode)

  if (!region) {
    notFound()
  }

  const product = await listProducts({
    countryCode,
    queryParams: { handle },
  }).then(({ response }) => response.products[0])

  // ...
}
```

This retrieves the country code using the `getCountryCode` utility, then passes it to the `getRegion` and `listProducts` functions.

Finally, update the `ProductPage` component to retrieve the country code using the `getCountryCode` utility and pass it to the used functions and components:

```tsx title="src/app/(main)/products/[handle]/page.tsx"
export default async function ProductPage(props: Props) {
  const params = await props.params
  const countryCode = await getCountryCode()
  const searchParams = await props.searchParams

  if (!countryCode) {
    notFound()
  }

  const region = await getRegion(countryCode)

  if (!region) {
    notFound()
  }

  const selectedVariantId = searchParams.v_id

  const pricedProduct = await listProducts({
    countryCode,
    queryParams: { handle: params.handle },
  }).then(({ response }) => response.products[0])

  const images = getImagesForVariant(pricedProduct, selectedVariantId)

  if (!pricedProduct) {
    notFound()
  }

  return (
    <ProductTemplate
      product={pricedProduct}
      region={region}
      countryCode={countryCode}
      images={images}
    />
  )
}
```

### d. Update Collection Page

In `src/app/(main)/collections/[handle]/page.tsx`, add the following import at the top of the file:

```tsx title="src/app/(main)/collections/[handle]/page.tsx"
import { getCountryCode } from "@lib/data/cookies"
```

Then, update the `Props` type to remove `countryCode` from the `params` prop:

```tsx title="src/app/(main)/collections/[handle]/page.tsx"
type Props = {
  params: Promise<{ handle: string }>
  searchParams: Promise<{
    page?: string
    sortBy?: SortOptions
  }>
}
```

Finally, update the `CollectionPage` component to retrieve the country code using the `getCountryCode` utility and pass it to the used component:

```tsx title="src/app/(main)/collections/[handle]/page.tsx"
export default async function CollectionPage(props: Props) {
  const searchParams = await props.searchParams
  const params = await props.params
  const { sortBy, page } = searchParams
  const countryCode = await getCountryCode()

  if (!countryCode) {
    notFound()
  }

  const collection = await getCollectionByHandle(params.handle).then(
    (collection: StoreCollection) => collection
  )

  if (!collection) {
    notFound()
  }

  return (
    <CollectionTemplate
      collection={collection}
      page={page}
      sortBy={sortBy}
      countryCode={countryCode}
    />
  )
}
```

This retrieves the country code using the `getCountryCode` utility and passes it to the `CollectionTemplate` component.

### e. Update Categories Page

In `src/app/(main)/categories/[...category]/page.tsx`, add the following import at the top of the file:

```tsx title="src/app/(main)/categories/[...category]/page.tsx"
import { getCountryCode } from "@lib/data/cookies"
```

Then, update the `Props` type to remove `countryCode` from the `params` prop:

```tsx title="src/app/(main)/categories/[...category]/page.tsx"
type Props = {
  params: Promise<{ category: string[] }>
  searchParams: Promise<{
    sortBy?: SortOptions
    page?: string
  }>
}
```

Finally, update the `CategoriesPage` component to retrieve the country code using the `getCountryCode` utility and pass it to the used component:

```tsx title="src/app/(main)/categories/[...category]/page.tsx"
export default async function CategoryPage(props: Props) {
  const searchParams = await props.searchParams
  const params = await props.params
  const { sortBy, page } = searchParams
  const countryCode = await getCountryCode()

  if (!countryCode) {
    notFound()
  }

  const productCategory = await getCategoryByHandle(params.category)

  if (!productCategory) {
    notFound()
  }

  return (
    <CategoryTemplate
      category={productCategory}
      sortBy={sortBy}
      page={page}
      countryCode={countryCode}
    />
  )
}
```

This retrieves the country code using the `getCountryCode` utility and passes it to the `CategoryTemplate` component.

### f. Update Addresses Page

In `src/app/(main)/account/@dashboard/addresses/page.tsx`, add the following import at the top of the file:

```tsx title="src/app/(main)/account/@dashboard/addresses/page.tsx"
import { getCountryCode } from "@lib/data/cookies"
```

Then, update the `Addresses` component to remove the `countryCode` parameter and retrieve the country code using the `getCountryCode` utility:

```tsx title="src/app/(main)/account/@dashboard/addresses/page.tsx"
export default async function Addresses() {
  const countryCode = await getCountryCode()
  const customer = await retrieveCustomer()

  if (!countryCode) {
    notFound()
  }

  // ...
}
```

***

## 6. Update Country Usage in Components

In this section, you'll update various components in the Next.js Starter Storefront to use the country code from cookies instead of the URL.

### a. Update AccountNav Component

In `src/modules/account/components/account-nav/index.tsx`, the country code is used to prefix URLs with the country code. You'll update it to remove the country code from the URLs.

In the `AccountNav` component, remove the country code retrieval using the `useParams` hook. You should have only the following lines before the `return` statement:

```tsx title="src/modules/account/components/account-nav/index.tsx"
const AccountNav = ({
  customer,
}: {
  customer: HttpTypes.StoreCustomer | null
}) => {
  const route = usePathname()

  const handleLogout = async () => {
    await signout()
  }

  // ...
}
```

Then, in the `return` statement of the `AccountNav` component, change the condition checking the `route` variable's value to the following:

```tsx title="src/modules/account/components/account-nav/index.tsx"
const AccountNav = ({
  customer,
}: {
  customer: HttpTypes.StoreCustomer | null
}) => {
  // ...
  return (
    <div>
      <div className="small:hidden" data-testid="mobile-account-nav">
        {route !== `/account` ? (
          {/* ... */}
        ) : (
          {/* ... */}
        )}
      </div>
      {/* ... */}
    </div>
  )
}
```

You remove the country code prefix from the URL checks in the `AccountNav` component.

Finally, in the `AccountNavLink` component defined in the same file, change the `active` variable declaration to the following:

```tsx title="src/modules/account/components/account-nav/index.tsx"
const AccountNavLink = ({
  href,
  route,
  children,
  "data-testid": dataTestId,
}: AccountNavLinkProps) => {
  const active = route === href
  
  // ...
}
```

You remove the country code prefix from the URL check in the `AccountNavLink` component.

### b. Update ProductActions Component

The `ProductActions` component uses the country code when adding products to the cart. You'll remove the need to pass the country code to the `addToCart` function. You'll update the `addToCart` function later to retrieve the country code from the cookie.

In `src/modules/products/components/product-actions/index.tsx`, remove the `countryCode` variable from the `ProductActions` component:

```tsx title="src/modules/products/components/product-actions/index.tsx"
export default function ProductActions({
  product,
  disabled,
}: ProductActionsProps) {
  // ...
  
  // REMOVE THIS LINE
  // const countryCode = useParams().countryCode as string

  // ...
}
```

Then, in the `handleAddToCart` function inside the `ProductActions` component, remove the `countryCode` argument when calling the `addToCart` function:

```tsx title="src/modules/products/components/product-actions/index.tsx"
export default function ProductActions({
  product,
  disabled,
}: ProductActionsProps) {
  // ...

  const handleAddToCart = async () => {
    if (!selectedVariant?.id) {return null}

    setIsAdding(true)

    await addToCart({
      variantId: selectedVariant.id,
      quantity: 1,
      // REMOVE THIS LINE
      // countryCode,
    })

    setIsAdding(false)
  }

  // ...
}
```

Ignore any type errors from the removed `countryCode` argument. You'll update the `addToCart` function later to remove the `countryCode` parameter.

### c. Update LocalizedClientLink Component

The `LocalizedClientLink` component creates links that include the country code prefix in URLs. Update it to remove the country code from the URLs.

In `src/modules/common/components/localized-client-link/index.tsx`, replace the content with the following:

```tsx title="src/modules/common/components/localized-client-link/index.tsx"
"use client"

import Link from "next/link"
import React from "react"

/**
 - Use this component to create a Next.js `<Link />` that works without country code in the URL.
 - Country code is now stored in a cookie instead.
 */
const LocalizedClientLink = ({
  children,
  href,
  ...props
}: {
  children?: React.ReactNode
  href: string
  className?: string
  onClick?: () => void
  passHref?: true
  [x: string]: any
}) => {
  return (
    <Link href={href} {...props}>
      {children}
    </Link>
  )
}

export default LocalizedClientLink
```

You make the following key changes:

1. Remove the `useParams` hook import and usage.
2. Remove the logic that adds the country code prefix to the `href` prop.

With these changes, the `LocalizedClientLink` component now creates links without the country code prefix in URLs.

***

## 7. Update Server Functions to Use Country Code from Cookie

Finally, you'll update the server functions in the Next.js Starter Storefront to retrieve the country code from the cookie instead of receiving it as a parameter.

### a. Update Customer Functions

In `src/lib/data/customer.ts`, find the `signout` function and update it to the following:

```ts title="src/lib/data/customer.ts"
export async function signout() {
  await sdk.auth.logout()

  await removeAuthToken()

  const customerCacheTag = await getCacheTag("customers")
  revalidateTag(customerCacheTag)

  await removeCartId()

  const cartCacheTag = await getCacheTag("carts")
  revalidateTag(cartCacheTag)

  redirect(`/account`)
}
```

You remove the `countryCode` parameter and the country code usage in the `redirect` function.

### b. Update Cart Functions

In `src/lib/data/cart.ts`, add the following import at the top of the file:

```ts title="src/lib/data/cart.ts"
import { 
  getCountryCode,
  setCountryCode,
} from "@lib/data/cookies"
```

Then, find the `addToCart` function and update it to the following:

```ts title="src/lib/data/cart.ts"
export async function addToCart({
  variantId,
  quantity,
}: {
  variantId: string
  quantity: number
}) {
  if (!variantId) {
    throw new Error("Missing variant ID when adding to cart")
  }

  const countryCode = await getCountryCode()

  if (!countryCode) {
    throw new Error("Country code not found. Please select a country.")
  }

  const cart = await getOrSetCart(countryCode)

  if (!cart) {
    throw new Error("Error retrieving or creating cart")
  }

  const headers = {
    ...(await getAuthHeaders()),
  }

  await sdk.store.cart
    .createLineItem(
      cart.id,
      {
        variant_id: variantId,
        quantity,
      },
      {},
      headers
    )
    .then(async () => {
      const cartCacheTag = await getCacheTag("carts")
      revalidateTag(cartCacheTag)

      const fulfillmentCacheTag = await getCacheTag("fulfillment")
      revalidateTag(fulfillmentCacheTag)
    })
    .catch(medusaError)
}
```

You remove the `countryCode` parameter and retrieve the country code using the `getCountryCode` utility inside the function.

Next, find the `placeOrder` function in the same file and update it to the following:

```ts title="src/lib/data/cart.ts"
export async function placeOrder(cartId?: string) {
  const id = cartId || (await getCartId())

  if (!id) {
    throw new Error("No existing cart found when placing an order")
  }

  const headers = {
    ...(await getAuthHeaders()),
  }

  const cartRes = await sdk.store.cart
    .complete(id, {}, headers)
    .then(async (cartRes) => {
      const cartCacheTag = await getCacheTag("carts")
      revalidateTag(cartCacheTag)
      return cartRes
    })
    .catch(medusaError)

  if (cartRes?.type === "order") {
    const orderCacheTag = await getCacheTag("orders")
    revalidateTag(orderCacheTag)

    removeCartId()
    redirect(`/order/${cartRes?.order.id}/confirmed`)
  }

  return cartRes.cart
}
```

You change the redirect logic to remove the country code from the URL.

Finally, find the `updateRegion` function in the same file and update it to the following:

```ts title="src/lib/data/cart.ts"
export async function updateRegion(countryCode: string, currentPath: string) {
  const cartId = await getCartId()
  const region = await getRegion(countryCode)

  if (!region) {
    throw new Error(`Region not found for country code: ${countryCode}`)
  }

  // Set country code cookie
  await setCountryCode(countryCode)

  if (cartId) {
    await updateCart({ region_id: region.id })
    const cartCacheTag = await getCacheTag("carts")
    revalidateTag(cartCacheTag)
  }

  const regionCacheTag = await getCacheTag("regions")
  revalidateTag(regionCacheTag)

  const productsCacheTag = await getCacheTag("products")
  revalidateTag(productsCacheTag)

  redirect(currentPath || "/")
}
```

You update the function to set the country code cookie using the `setCountryCode` utility instead of relying on the URL, and you remove the country code from the redirect URL.

***

## Test Your Changes

After completing these steps, you can use the Next.js Starter Storefront without country code prefixes in URLs. The country code is now managed using cookies.

To test it, run the following command in the directory of the Medusa application that the storefront connects to:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

Then, in a separate terminal, run the following command in the Next.js Starter Storefront directory to start the development server:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

You should be able to navigate the storefront, select different countries using the country selector, go through checkout, and place orders without country code prefixes in URLs. The country code is stored and retrieved using cookies as intended.


# Revalidate Cache in Next.js Starter Storefront

In this guide, you'll learn about the general approach to revalidating cache in the Next.js Starter Storefront when data is updated in the Medusa application.

## Approach Overview

By default, the data that the Next.js Starter Storefront retrieves from the Medusa application is cached in the browser. This cache is used to improve the performance and speed of the storefront.

In some cases, you may need to revalidate the cache in the storefront when data is updated in the Medusa application. For example, when a product variant's price is updated in the Medusa application, you may want to revalidate the cache in the storefront to reflect the updated price.

You're free to choose the approach that works for your use case, custom requirements, and tech stack. The approach that Medusa recommends is:

1. Create a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) in the Medusa application that listens for the event that triggers the data update. For example, you can listen to the `product.updated` event.
2. In the subscriber, send a request to a custom endpoint in the Next.js Starter Storefront to trigger the cache revalidation.
3. Create the custom endpoint in the Next.js Starter Storefront that listens for the request from the subscriber and revalidates the cache.

Refer to the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for a full list of events that the Medusa application emits.

***

## Example: Revalidating Cache for Product Update

Consider you want to revalidate the cache in the Next.js Starter Storefront whenever a product is updated.

Start by creating the following subscriber in the Medusa application:

```ts
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"

export default async function productUpdatedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  // send request to next.js storefront to revalidate cache
  await fetch(`${process.env.STOREFRONT_URL}/api/revalidate?tags=products`)
}

export const config: SubscriberConfig = {
  event: "product.updated",
}
```

In the subscriber, you send a request to the custom endpoint `/api/revalidate` in the Next.js Starter Storefront. The request includes the query parameter `tags=product-${data.id}` to specify the cache that needs to be revalidated.

Make sure to set the `STOREFRONT_URL` environment variable in the Medusa application to the URL of the Next.js Starter Storefront.

Then, create in the Next.js Starter Storefront the custom endpoint that listens for the request and revalidates the cache:

```ts title="src/app/api/revalidate/route.ts"
import { NextRequest, NextResponse } from "next/server"
import { revalidatePath } from "next/cache"

export async function GET(req: NextRequest) {
  const searchParams = req.nextUrl.searchParams
  const tags = searchParams.get("tags") as string

  if (!tags) {
    return NextResponse.json({ error: "No tags provided" }, { status: 400 })
  }

  const tagsArray = tags.split(",")
  await Promise.all(
    tagsArray.map(async (tag) => {
      switch (tag) {
        case "products":
          revalidatePath("/[countryCode]/(main)/store", "page")
          revalidatePath("/[countryCode]/(main)/products/[handle]", "page")
        // TODO add for other tags
      }
    })
  )

  return NextResponse.json({ message: "Revalidated" }, { status: 200 })
}
```

In this example, you create a custom endpoint `/api/revalidate` that revalidates the cache for paths based on the tags passed as query parameters.

You only handle the case of the `products` tag in this example, but you can extend the switch statement to handle other tags as needed.

To revalidate the cache, you use Next.js's `revalidatePath` function. Learn more about in the [Next.js documentation](https://nextjs.org/docs/app/api-reference/functions/revalidatePath).

### Test it Out

To test out this mechanism, run the Medusa application and the Next.js Starter Storefront.

Then, update a product in the Medusa application. You can see in the Next.js Starter Storefront's terminal that a request was sent to the `/api/revalidate` endpoint, meaning that the cache was revalidated successfully.


# Create Order Returns in the Storefront

In this tutorial, you'll learn how to let customers create order returns directly from your storefront.

Medusa supports automated Return Merchandise Authorization (RMA) flows for orders. Customers can create return requests for their orders, and merchants can manage these requests through the Medusa Admin dashboard. Medusa provides the necessary API routes and workflows to handle returns efficiently.

## Summary

In this tutorial, you'll customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to let customers create return requests for their orders directly from the storefront.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Request return page in storefront](https://res.cloudinary.com/dza7lstvk/image/upload/v1758009041/Medusa%20Resources/CleanShot_2025-09-16_at_10.50.27_2x_g4rwjr.png)

[Example Repository](https://github.com/medusajs/examples/tree/main/returns-storefront): Find the full code of the guide in this repository.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.

Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Add Return Server Functions

In this step, you'll add server functions to the Next.js Starter Storefront that let you send requests to the Medusa application for order returns. You'll use these functions later in the storefront pages.

If you installed the Next.js Starter Storefront with the Medusa backend, the storefront was installed in a separate directory. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-returns`, you can find the storefront by going back to the parent directory and changing to the `medusa-returns-storefront` directory:

```bash
cd ../medusa-returns-storefront # change based on your project name
```

### List Return Reasons Function

The first function sends a request to the [List Return Reasons](https://docs.medusajs.com/api/store#return-reasons_getreturnreasons) API route. It fetches the available return reasons, which customers can select from when creating a return request.

Create the file `src/lib/data/returns.ts` with the following content:

```ts title="src/lib/data/returns.ts"
"use server"

import { sdk } from "@lib/config"
import { getAuthHeaders, getCacheOptions } from "@lib/data/cookies"
import medusaError from "@lib/util/medusa-error"
import { HttpTypes } from "@medusajs/types"

export const listReturnReasons = async () => {
  const headers = {
    ...(await getAuthHeaders()),
  }

  const next = {
    ...(await getCacheOptions("return-reasons")),
  }

  return sdk.client
    .fetch<HttpTypes.StoreReturnReasonListResponse>(`/store/return-reasons`, {
      method: "GET",
      headers,
      next,
      cache: "force-cache",
    })
    .then(({ return_reasons }) => return_reasons)
    .catch((err) => medusaError(err))
}
```

You add the `listReturnReasons` function. It sends a `GET` request to the `/store/return-reasons` API route. The function returns the list of return reasons.

### List Return Shipping Options Function

Next, you'll add a function that sends a request to the [List Shipping Options](https://docs.medusajs.com/api/store#shipping-options_getshippingoptions) API route. It fetches the available shipping options for returns, which customers can select from when creating a return request.

In the same `src/lib/data/returns.ts` file, add the following function:

```ts title="src/lib/data/returns.ts"
export const listReturnShippingOptions = async (cartId: string) => {
  const headers = {
    ...(await getAuthHeaders()),
  }

  const next = {
    ...(await getCacheOptions("shipping-options")),
  }

  return sdk.client
    .fetch<HttpTypes.StoreShippingOptionListResponse>(`/store/shipping-options`, {
      method: "GET",
      query: {
        cart_id: cartId,
        is_return: true,
      },
      headers,
      next,
      cache: "force-cache",
    })
    .then(({ shipping_options }) => shipping_options)
    .catch((err) => medusaError(err))
}
```

You add the `listReturnShippingOptions` function. It sends a `GET` request to the `/store/shipping-options` API route and returns the shipping options.

The API route accepts the following query parameters:

- `cart_id`: The ID of the cart associated with the order being returned.
- `is_return`: A boolean value set to `true` to retrieve only shipping options for returns.

### Create Return Function

Finally, you'll add a function that sends a request to the [Create Return](https://docs.medusajs.com/api/store#returns_postreturns) API route. This creates a return request for an order.

In the same `src/lib/data/returns.ts` file, add the following function:

```ts title="src/lib/data/returns.ts"
export const createReturnRequest = async (
  state: {
    success: boolean
    error: string | null
    return: any | null
  },
  formData: FormData
): Promise<{
  success: boolean
  error: string | null
  return: any | null
}> => {
  const orderId = formData.get("order_id") as string
  const items = JSON.parse(formData.get("items") as string)
  const returnShippingOptionId = formData.get("return_shipping_option_id") as string
  const locationId = formData.get("location_id") as string

  if (!orderId || !items || !returnShippingOptionId) {
    return { 
      success: false, 
      error: "Order ID, items, and return shipping option are required", 
      return: null, 
    }
  }

  const headers = await getAuthHeaders()

  return await sdk.client
    .fetch<HttpTypes.StoreReturnResponse>(`/store/returns`, {
      method: "POST",
      body: {
        order_id: orderId,
        items,
        return_shipping: {
          option_id: returnShippingOptionId,
        },
        location_id: locationId,
      },
      headers,
    })
    .then(({ return: returnData }) => ({ 
      success: true, 
      error: null, 
      return: returnData, 
    }))
    .catch((err) => ({ 
      success: false, 
      error: err.message, 
      return: null, 
    }))
}
```

You add the `createReturnRequest` function. It sends a `POST` request to the `/store/returns` API route. The function accepts a `FormData` object containing the order ID, the items to be returned, the selected return shipping option ID, and the stock location ID to which the returned items will be sent.

The function returns an object with the following properties:

- `success`: Whether the return request was created successfully.
- `error`: The error message if the request failed, or `null` if it succeeded.
- `return`: The created return object if the request succeeded, or `null` if it failed.

***

## Step 3: Add Return Utilities

In this step, you'll add a file with utility functions that will mainly help you determine whether an order and its items are eligible for return.

An item is eligible for return if it has been delivered and not yet returned. An order is eligible for return if it has at least one item that's eligible for return.

Create the file `src/lib/util/returns.ts` with the following content:

```ts title="src/lib/util/returns.ts" highlights={returnUtilsHighlights}
import { HttpTypes } from "@medusajs/types"

export type ItemWithDeliveryStatus = HttpTypes.StoreOrderLineItem & {
  deliveredQuantity: number
  returnableQuantity: number
  isDelivered: boolean
  isReturnable: boolean
}

export const calculateReturnableQuantity = (item: HttpTypes.StoreOrderLineItem): number => {
  const deliveredQuantity = item.detail?.delivered_quantity || 0
  const returnRequestedQuantity = item.detail?.return_requested_quantity || 0
  const returnReceivedQuantity = item.detail?.return_received_quantity || 0
  const writtenOffQuantity = item.detail?.written_off_quantity || 0

  return Math.max(
    0, 
    deliveredQuantity - returnRequestedQuantity - returnReceivedQuantity - writtenOffQuantity
  )
}

export const isItemReturnable = (item: HttpTypes.StoreOrderLineItem): boolean => {
  return calculateReturnableQuantity(item) > 0
}

export const hasReturnableItems = (order: HttpTypes.StoreOrder): boolean => {
  return order.items?.some(isItemReturnable) || false
}

export const enhanceItemsWithReturnStatus = (items: HttpTypes.StoreOrderLineItem[]): ItemWithDeliveryStatus[] => {
  return items.map((item) => {
    const deliveredQuantity = item.detail?.delivered_quantity || 0
    const returnableQuantity = calculateReturnableQuantity(item)

    return {
      ...item,
      deliveredQuantity,
      returnableQuantity,
      isDelivered: deliveredQuantity > 0,
      isReturnable: returnableQuantity > 0,
    }
  })
}
```

You add the following utility functions:

- `calculateReturnableQuantity`: Calculates the returnable quantity for a given item. It subtracts quantities that have been returned, requested for return, or written off from the delivered quantity.
- `isItemReturnable`: Determines if a given item is returnable by checking if its returnable quantity is greater than zero.
- `hasReturnableItems`: Checks if an order has at least one item that is returnable.
- `enhanceItemsWithReturnStatus`: Adds return status information to each item in the order. It adds the following properties:
  - `deliveredQuantity`: The quantity of the item that has been delivered.
  - `returnableQuantity`: The quantity of the item that is returnable.
  - `isDelivered`: A boolean indicating whether any quantity of the item has been delivered.
  - `isReturnable`: A boolean indicating whether any quantity of the item is returnable.

You'll use these utility functions later in the storefront pages. They help determine whether to allow customers to create a return request for their order. They also help determine which items can be returned.

***

## Step 4: Add Return Item Selector

In this step, you'll add a component that lets customers select the quantity to return of items from their order. Later, you'll use this component in the return request page.

![Preview of the return item selector on the request return page](https://res.cloudinary.com/dza7lstvk/image/upload/v1758008962/Medusa%20Resources/CleanShot_2025-09-16_at_10.48.41_2x_myun8q.png)

The component displays each item's details. It lets customers specify the quantity to return based on the returnable quantity of the item. It also lets customers select a return reason for the item and provide an optional note.

To create the component, create the file `src/modules/account/components/return-item-selector/index.tsx` with the following content:

```tsx title="src/modules/account/components/return-item-selector/index.tsx" highlights={returnItemSelectorHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
"use client"

import { HttpTypes } from "@medusajs/types"
import { Badge, IconButton, Select, Textarea } from "@medusajs/ui"
import { Minus, Plus } from "@medusajs/icons"
import Thumbnail from "@modules/products/components/thumbnail"
import { convertToLocale } from "@lib/util/money"
import { ItemWithDeliveryStatus } from "../../../../lib/util/returns"

export type ReturnItemSelection = {
  id: string
  quantity: number
  return_reason_id?: string
  note?: string
}

type ReturnItemSelectorProps = {
  items: ItemWithDeliveryStatus[]
  returnReasons: HttpTypes.StoreReturnReason[]
  onItemSelectionChange: (item: ReturnItemSelection) => void
  selectedItems: ReturnItemSelection[]
}

const ReturnItemSelector: React.FC<ReturnItemSelectorProps> = ({
  items,
  returnReasons,
  onItemSelectionChange,
  selectedItems,
}) => {
  const handleQuantityChange = ({
    item_id,
    quantity,
    selected_item,
  }: {
    item_id: string
    quantity: number
    selected_item?: ReturnItemSelection
  }) => {
    const item = items.find((i) => i.id === item_id)
    if (!item || !item.isReturnable) {return}

    const maxQuantity = item.returnableQuantity
    const newQuantity = Math.max(0, Math.min(quantity, maxQuantity))
    
    onItemSelectionChange({
      id: item_id,
      quantity: newQuantity,
      return_reason_id: selected_item?.return_reason_id || "",
      note: selected_item?.note || "",
    })
  }

  const handleReturnReasonChange = ({
    item_id,
    return_reason_id,
    selected_item,
  }: {
    item_id: string
    return_reason_id: string
    selected_item?: ReturnItemSelection
  }) => {
    onItemSelectionChange({
      id: item_id,
      quantity: selected_item?.quantity || 0,
      return_reason_id,
      note: selected_item?.note || "",
    })
  }

  const handleNoteChange = ({
    item_id,
    note,
    selected_item,
  }: {
    item_id: string
    note: string
    selected_item?: ReturnItemSelection
  }) => {
    onItemSelectionChange({
      id: item_id,
      quantity: selected_item?.quantity || 0,
      return_reason_id: selected_item?.return_reason_id || "",
      note,
    })
  }

  // TODO render component
}

export default ReturnItemSelector
```

You create the `ReturnItemSelector` component that accepts the following props:

- `items`: The list of items in the order, enhanced with their delivery and return status.
- `returnReasons`: The list of available return reasons.
- `onItemSelectionChange`: A callback function that is called when the customer selects or updates an item to return.
- `selectedItems`: The list of items that the customer has selected to return.

In the component, you define three functions:

- `handleQuantityChange`: Called when the customer changes the quantity to return for an item.
- `handleReturnReasonChange`: Called when the customer selects a return reason for an item.
- `handleNoteChange`: Called when the customer adds or updates a note for an item.

Next, you'll add a `return` statement that renders the item selector. Replace the `TODO` in the `ReturnItemSelector` component with the following:

```tsx title="src/modules/account/components/return-item-selector/index.tsx"
return (
  <div className="space-y-4">
    {items.map((item) => {
      const itemSelection = selectedItems.find((si) => si.id === item.id)
      const currentQuantity = itemSelection?.quantity || 0
      const currentReturnReason = itemSelection?.return_reason_id || ""
      const currentNote = itemSelection?.note || ""

      return (
        <div
          key={item.id}
          className={`p-4 border rounded-lg ${
            !item.isReturnable ? "opacity-60 bg-gray-50" : ""
          }`}
        >
          <div className="flex items-start gap-4">
            <div className="flex-shrink-0">
              <div className="flex w-16">
                <Thumbnail thumbnail={item.thumbnail} images={[]} size="square" />
              </div>
            </div>
            
            <div className="flex-1 min-w-0">
              <div className="flex items-center gap-2 mb-1">
                <h4 className="txt-medium truncate">
                  {item.title}
                </h4>
                {!item.isReturnable && (
                  // @ts-ignore
                  <Badge color="grey" size="small">
                    {!item.isDelivered ? "Not delivered" : "Not returnable"}
                  </Badge>
                )}
              </div>
              {item.variant && (
                <p className="txt-small text-ui-fg-subtle">
                  {item.variant.title}
                </p>
              )}
              <p className="txt-small text-ui-fg-subtle">
                {item.isReturnable ? (
                  <>Available to return: {item.returnableQuantity} {item.returnableQuantity === 1 ? "item" : "items"}</>
                ) : item.isDelivered ? (
                  <>Delivered: {item.deliveredQuantity} of {item.quantity} {item.quantity === 1 ? "item" : "items"} (already processed)</>
                ) : (
                  <>Delivered: 0 of {item.quantity} {item.quantity === 1 ? "item" : "items"}</>
                )}
              </p>
            </div>

            <div className="flex items-center gap-2">
              <span className="txt-small">
                {convertToLocale({
                  amount: item.unit_price,
                  currency_code: "USD", // Default currency, should be passed from parent
                })}
              </span>
              
              {item.isReturnable ? (
                <div className="flex items-center gap-2">
                  {/* @ts-ignore */}
                  <IconButton
                    size="small"
                    onClick={() => handleQuantityChange({
                      item_id: item.id,
                      quantity: currentQuantity - 1,
                      selected_item: itemSelection,
                    })}
                    disabled={currentQuantity <= 0}
                  >
                    <Minus />
                  </IconButton>
                  
                  <span className="w-8 text-center txt-small">
                    {currentQuantity}
                  </span>
                  
                  {/* @ts-ignore */}
                  <IconButton
                    size="small"
                    onClick={() => handleQuantityChange({
                      item_id: item.id,
                      quantity: currentQuantity + 1,
                      selected_item: itemSelection,
                    })}
                    disabled={currentQuantity >= item.returnableQuantity}
                  >
                    <Plus />
                  </IconButton>
                </div>
              ) : (
                <div className="txt-small text-ui-fg-subtle">
                  Not available
                </div>
              )}
            </div>
          </div>

          {item.isReturnable && currentQuantity > 0 && (
            <div className="mt-4 pt-4 border-t border-ui-border-base space-y-3">
              <div>
                <label className="block txt-small-plus mb-2">
                  Return Reason (Optional)
                </label>
                <Select
                  value={currentReturnReason}
                  onValueChange={(value) => handleReturnReasonChange({
                    item_id: item.id,
                    return_reason_id: value,
                    selected_item: itemSelection,
                  })}
                >
                  <Select.Trigger>
                    <Select.Value placeholder="Select a reason..." />
                  </Select.Trigger>
                  <Select.Content>
                    {returnReasons.map((reason) => (
                      <Select.Item key={reason.id} value={reason.id}>
                        {reason.label}
                      </Select.Item>
                    ))}
                  </Select.Content>
                </Select>
              </div>
              
              <div>
                <label className="block txt-small-plus mb-2">
                  Additional Note (Optional)
                </label>
                <Textarea
                  value={currentNote}
                  onChange={(e) => handleNoteChange({
                    item_id: item.id,
                    note: e.target.value,
                    selected_item: itemSelection,
                  })}
                  placeholder="Please provide any additional information about this return..."
                  rows={2}
                  maxLength={500}
                />
                <p className="txt-xsmall text-ui-fg-subtle mt-1">
                  {currentNote.length}/500 characters
                </p>
              </div>
            </div>
          )}
        </div>
      )
    })}
    
    {selectedItems.length > 0 && (
      <div className="mt-4 p-4 bg-gray-50 rounded-lg">
        <h4 className="txt-medium-plus mb-2">Selected Items Summary</h4>
        <div className="space-y-3">
          {selectedItems.map((selectedItem) => {
            const item = items.find((i) => i.id === selectedItem.id)
            if (!item) {return null}
            
            const returnReason = returnReasons.find((r) => r.id === selectedItem.return_reason_id)
            
            return (
              <div key={selectedItem.id} className="border-b border-gray-200 pb-2 last:border-b-0">
                <div className="flex justify-between txt-small mb-1">
                  <span>{item.title} x {selectedItem.quantity}</span>
                  <span>
                    {convertToLocale({
                      amount: item.unit_price * selectedItem.quantity,
                      currency_code: "USD", // Default currency, should be passed from parent
                    })}
                  </span>
                </div>
                {returnReason && (
                  <p className="txt-xsmall text-ui-fg-subtle">
                    Reason: {returnReason.label}
                  </p>
                )}
                {selectedItem.note && (
                  <p className="txt-xsmall text-ui-fg-subtle">
                    Note: {selectedItem.note}
                  </p>
                )}
              </div>
            )
          })}
        </div>
      </div>
    )}
  </div>
)
```

You render the list of items in the order. For each item, you display its details, including its title, variant, price, and returnable status.

If the item is returnable, you display controls that let customers select the quantity to return, choose a return reason from a dropdown, and provide an optional note.

You also display a summary of the selected items to return at the bottom of the component.

***

## Step 5: Add Return Shipping Selector

In this step, you'll add a component that lets customers select a shipping option for their return request. Later, you'll use this component in the return request page.

![Preview of the return shipping selector on the request return page](https://res.cloudinary.com/dza7lstvk/image/upload/v1758010574/Medusa%20Resources/CleanShot_2025-09-16_at_11.13.19_2x_tvxnrq.png)

The component will display the available shipping options for returns, along with their name and price. For shipping options whose price type is calculated by the associated fulfillment provider, the component will retrieve its calculated price from the Medusa application.

To create the component, create the file `src/modules/account/components/return-shipping-selector/index.tsx` with the following content:

```tsx title="src/modules/account/components/return-shipping-selector/index.tsx" highlights={returnShippingSelectorHighlights} collapsibleLines="1-11" expandButtonLabel="Show Imports"
"use client"

import React, { 
  useEffect, 
  useState,
} from "react"
import { RadioGroup } from "@headlessui/react"
import { HttpTypes } from "@medusajs/types"
import { clx } from "@medusajs/ui"
import { convertToLocale } from "@lib/util/money"
import { calculatePriceForShippingOption } from "@lib/data/fulfillment"
import { Loader } from "@medusajs/icons"
import Radio from "@modules/common/components/radio"

type ReturnShippingSelectorProps = {
  shippingOptions: HttpTypes.StoreCartShippingOption[]
  selectedOption: string
  onOptionSelect: (optionId: string) => void
  cartId: string
  currencyCode: string
}

const ReturnShippingSelector: React.FC<ReturnShippingSelectorProps> = ({
  shippingOptions,
  selectedOption,
  onOptionSelect,
  cartId,
  currencyCode,
}) => {
  const [isLoadingPrices, setIsLoadingPrices] = useState(true)
  const [calculatedPricesMap, setCalculatedPricesMap] = useState<
    Record<string, number>
  >({})

  useEffect(() => {
    setIsLoadingPrices(true)

    if (shippingOptions?.length) {
      const promises = shippingOptions
        .filter((sm) => sm.price_type === "calculated")
        .map((sm) => calculatePriceForShippingOption(sm.id, cartId))

      if (promises.length) {
        Promise.allSettled(promises).then((res) => {
          const pricesMap: Record<string, number> = {}
          res
            .filter((r) => r.status === "fulfilled")
            .forEach((p) => (pricesMap[p.value?.id || ""] = p.value?.amount!))

          setCalculatedPricesMap(pricesMap)
          setIsLoadingPrices(false)
        })
      } else {
        setIsLoadingPrices(false)
      }
    } else {
      setIsLoadingPrices(false)
    }
  }, [shippingOptions, cartId])
  
  if (shippingOptions.length === 0) {
    return (
      <div className="p-4 border border-yellow-200 bg-yellow-50 rounded-lg">
        <p className="text-yellow-800 text-sm">
          No return shipping options are currently available. Please contact customer service for assistance.
        </p>
      </div>
    )
  }

  // TODO render component
}

export default ReturnShippingSelector
```

You create the `ReturnShippingSelector` component that accepts the following props:

- `shippingOptions`: The list of available shipping options for returns.
- `selectedOption`: The ID of the currently selected shipping option.
- `onOptionSelect`: A callback function that is called when the customer selects a shipping option.
- `cartId`: The ID of the cart associated with the order being returned.
- `currencyCode`: The currency code to use for displaying prices.

In the component, you define a `useEffect` hook that retrieves the calculated prices for shipping options with price type `calculated`. The hook updates the component's state with the retrieved prices.

Next, you'll add a `return` statement that renders the shipping selector. Replace the `TODO` in the `ReturnShippingSelector` component with the following:

```tsx title="src/modules/account/components/return-shipping-selector/index.tsx"
return (
  <RadioGroup
    value={selectedOption}
    onChange={onOptionSelect}
  >
    <div className="space-y-3">
      {shippingOptions.map((option) => (
        <RadioGroup.Option
          key={option.id}
          value={option.id}
          className={clx(
            "p-4 border rounded-lg cursor-pointer transition-colors",
            {
              "border-ui-fg-interactive bg-ui-bg-interactive/5":
                selectedOption === option.id,
              "border-gray-200 hover:border-gray-300":
                selectedOption !== option.id,
            }
          )}
        >
          <div className="flex items-center gap-3">
            <Radio
              checked={selectedOption === option.id}
              data-testid={`shipping-option-${option.id}`}
            />
            
            <div className="flex-1">
              <div className="flex items-center justify-between">
                <h4 className="txt-medium">
                  {option.name}
                </h4>
                <span className="txt-medium">
                  {option.price_type === "flat" ? (
                    convertToLocale({
                      amount: option.amount!,
                      currency_code: currencyCode,
                    })
                  ) : calculatedPricesMap[option.id] ? (
                    convertToLocale({
                      amount: calculatedPricesMap[option.id],
                      currency_code: currencyCode,
                    })
                  ) : isLoadingPrices ? (
                    <Loader />
                  ) : (
                    "-"
                  )}
                </span>
              </div>
              
              {option.data && typeof option.data === "object" && option.data !== null && "description" in option.data && (
                <p className="txt-small mt-1">
                  {String(option.data.description)}
                </p>
              )}
            </div>
          </div>
        </RadioGroup.Option>
      ))}
    </div>
  </RadioGroup>
)
```

You render the list of available shipping options for returns. For each option, you display its name and price.

If a shipping option's price type is `calculated`, you display the calculated price retrieved from the Medusa application.

***

## Step 6: Add Request Return Page

In this step, you'll add the page that lets customers create a return request for their order. You'll add a template component that contains the main logic of the page, and a page component that fetches the necessary data and renders the template.

### Request Return Template

First, you'll add the template component. Create the file `src/modules/account/templates/return-request-template.tsx` with the following content:

```tsx title="src/modules/account/templates/return-request-template.tsx" highlights={returnRequestTemplateHighlights} collapsibleLines="1-13" expandButtonLabel="Show Imports"
"use client"

import React, { useState, useActionState } from "react"
import { XMark } from "@medusajs/icons"
import { HttpTypes } from "@medusajs/types"
import { createReturnRequest } from "@lib/data/returns"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import ReturnItemSelector, { ReturnItemSelection } from "@modules/account/components/return-item-selector"
import ReturnShippingSelector from "@modules/account/components/return-shipping-selector"
import { convertToLocale } from "@lib/util/money"
import { enhanceItemsWithReturnStatus } from "@lib/util/returns"
import { Button } from "@medusajs/ui"

type ReturnRequestTemplateProps = {
  order: HttpTypes.StoreOrder
  shippingOptions: HttpTypes.StoreCartShippingOption[]
  returnReasons: HttpTypes.StoreReturnReason[]
}

const ReturnRequestTemplate: React.FC<ReturnRequestTemplateProps> = ({
  order,
  shippingOptions,
  returnReasons,
}) => {
  const [selectedItems, setSelectedItems] = useState<ReturnItemSelection[]>([])
  const [selectedShippingOption, setSelectedShippingOption] = useState("")
  const [state, formAction] = useActionState(createReturnRequest, {
    success: false,
    error: null,
    return: null,
  })

  // Get all items and categorize them based on delivered quantity and returnable quantity
  const itemsWithDeliveryStatus = enhanceItemsWithReturnStatus(order.items || [])

  const handleItemSelection = ({
    id,
    quantity,
    return_reason_id,
    note,
  }: ReturnItemSelection) => {
    setSelectedItems((prev) => {
      const existing = prev.find((item) => item.id === id)
      if (existing) {
        if (quantity === 0) {
          return prev.filter((item) => item.id !== id)
        }
        return prev.map((item) => {
          return item.id === id ? 
            { ...item, quantity, return_reason_id, note } : 
            item
        })
      } else if (quantity > 0) {
        return [...prev, { id, quantity, return_reason_id, note }]
      }
      return prev
    })
  }

  const handleSubmit = (formData: FormData) => {
    formData.append("order_id", order.id)
    formData.append("items", JSON.stringify(selectedItems))
    formData.append("return_shipping_option_id", selectedShippingOption)
    // @ts-expect-error issue in HTTP types
    const locationId = shippingOptions.find((opt) => opt.id === selectedShippingOption)?.service_zone.fulfillment_set.location.id
    formData.append("location_id", locationId)
    formAction(formData)
  }

  // TODO render component

}

export default ReturnRequestTemplate
```

You create the `ReturnRequestTemplate` component that accepts the following props:

- `order`: The order for which the return request is being created.
- `shippingOptions`: The list of available shipping options for returns.
- `returnReasons`: The list of available return reasons.

In the component, you define the following variables and functions:

- `selectedItems`: A state variable that holds the list of items customers have selected to return.
- `selectedShippingOption`: A state variable that holds the ID of the selected shipping option for the return.
- `state` and `formAction`: Variables returned by React's `useActionState` hook that manage the state of the return request creation action.
- `itemsWithDeliveryStatus`: A variable that holds the list of items in the order, enhanced with their delivery and return status using the `enhanceItemsWithReturnStatus` utility function.
- `handleItemSelection`: A function that updates the `selectedItems` state when customers select or update an item to return.
- `handleSubmit`: A function that handles the form submission to create the return request. It appends the necessary data to a `FormData` object and calls the `formAction` function.

Next, you'll add a `return` statement that shows a success message after the return request is created successfully. Replace the `TODO` in the `ReturnRequestTemplate` component with the following:

```tsx title="src/modules/account/templates/return-request-template.tsx"
if (state.success && state.return) {
  return (
    <div className="flex flex-col justify-center gap-y-4">
      <div className="flex gap-2 justify-between items-center">
        <h1 className="text-2xl-semi">Return Request Submitted</h1>
        <LocalizedClientLink
          href="/account/orders"
          className="flex gap-2 items-center text-ui-fg-subtle hover:text-ui-fg-base"
        >
          <XMark /> Back to orders
        </LocalizedClientLink>
      </div>
      <div className="bg-white p-6 rounded-lg border">
        <div className="text-center">
          <h2 className="text-xl-semi mb-4">Return Request Created Successfully</h2>
          <p className="text-base-regular mb-4">
            Your return request has been submitted. Return ID: <strong>{state.return.id}</strong>
          </p>
          <p className="text-small-regular text-ui-fg-subtle">
            Our support team may contact you for further information regarding your return.
          </p>
        </div>
      </div>
    </div>
  )
}

// TODO render component
```

If the return request was created successfully, you display a success message with the return ID and a link to go back to the orders page.

Finally, you'll add a `return` statement that renders the main content of the return request page. Replace the `TODO` in the `ReturnRequestTemplate` component with the following:

```tsx title="src/modules/account/templates/return-request-template.tsx"
return (
  <div className="flex flex-col justify-center gap-y-4">
    <div className="flex gap-2 justify-between items-center">
      <h1 className="text-2xl-semi">Request Return</h1>
      <LocalizedClientLink
        href={`/account/orders/details/${order.id}`}
        className="flex gap-2 items-center text-ui-fg-subtle hover:text-ui-fg-base"
      >
        <XMark /> Back to order details
      </LocalizedClientLink>
    </div>

    <div>
      <div className="mb-6">
        <h2 className="text-xl-semi mb-2">Order #{order.display_id}</h2>
        <div className="flex items-center gap-4 text-small-regular text-ui-fg-subtle mb-4">
          <span>Ordered: {new Date(order.created_at).toDateString()}</span>
          <span>Total: {convertToLocale({
            amount: order.total,
            currency_code: order.currency_code,
          })}</span>
        </div>
        <p className="text-base-regular text-ui-fg-subtle">
          You can request a return for items that have been delivered. Select the items you'd like to return and choose a return shipping option.
        </p>
      </div>

      {itemsWithDeliveryStatus.length === 0 ? (
        <div className="text-center py-8">
          <p className="text-base-regular text-ui-fg-subtle">
            No items available in this order.
          </p>
        </div>
      ) : (
        <form action={handleSubmit} className="space-y-6">
          <div>
            <h3 className="txt-medium-plus mb-4">Items to Return</h3>
            <ReturnItemSelector
              items={itemsWithDeliveryStatus}
              returnReasons={returnReasons}
              onItemSelectionChange={handleItemSelection}
              selectedItems={selectedItems}
            />
          </div>

          <div>
            <h3 className="txt-medium-plus mb-4">Choose Return Shipping</h3>
            <ReturnShippingSelector
              shippingOptions={shippingOptions}
              selectedOption={selectedShippingOption}
              onOptionSelect={setSelectedShippingOption}
              cartId={(order as any).cart.id}
              currencyCode={order.currency_code}
            />
          </div>


          {state.error && (
            <div className="bg-red-50 border border-red-200 rounded-md p-4">
              <p className="text-red-800 text-sm">{state.error}</p>
            </div>
          )}

          <div className="flex justify-end">
            <Button 
              type="submit" 
              variant="primary"
              disabled={selectedItems.length === 0 || selectedShippingOption === ""}
            >
              Request Return
            </Button>
          </div>
        </form>
      )}
    </div>
  </div>
)
```

You render the main content of the return request page. You display the order details, including the order ID, order date, and total amount.

You also display the item and shipping selector components that you created earlier.

Finally, you show a submit button that lets customers submit the return request.

### Request Return Page

Next, you'll add the page component that fetches the necessary data and renders the `ReturnRequestTemplate` component.

Create the file `src/app/[countryCode]/(main)/account/@dashboard/orders/return/[id]/page.tsx` with the following content:

```tsx title="src/app/[countryCode]/(main)/account/@dashboard/orders/return/[id]/page.tsx" collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { retrieveOrder } from "@lib/data/orders"
import { listReturnShippingOptions, listReturnReasons } from "@lib/data/returns"
import ReturnRequestTemplate from "@modules/account/templates/return-request-template"
import { Metadata } from "next"
import { notFound } from "next/navigation"

type Props = {
  params: Promise<{ id: string }>
}

export async function generateMetadata(props: Props): Promise<Metadata> {
  const params = await props.params
  const order = await retrieveOrder(params.id).catch(() => null)

  if (!order) {
    notFound()
  }

  return {
    title: `Return Request - Order #${order.display_id}`,
    description: `Request a return for your order`,
  }
}

export default async function ReturnRequestPage(props: Props) {
  const params = await props.params
  
  const order = await retrieveOrder(params.id).catch(() => null)

  if (!order) {
    return notFound()
  }

  // Get shipping options and return reasons
  const [shippingOptions, returnReasons] = await Promise.all([
    listReturnShippingOptions((order as any).cart.id),
    listReturnReasons(),
  ])

  return <ReturnRequestTemplate order={order} shippingOptions={shippingOptions} returnReasons={returnReasons} />
}
```

You create the `ReturnRequestPage` component that fetches the order details, available return shipping options, and return reasons.

If the order is not found, you return a 404 page. Otherwise, you render the `ReturnRequestTemplate` component with the fetched data.

### Retrieve Cart with Order Data

Since you need the cart ID to retrieve data like return shipping options, you need to retrieve the cart along with the order data. You can do this by adding `+cart.id` to the `fields` query parameter when retrieving the order.

In `src/lib/data/orders.ts`, update the `retrieveOrder` function to include the cart in the `fields` parameter:

```ts title="src/lib/data/orders.ts" highlights={[["8"]]}
export const retrieveOrder = async (id: string) => {
  // ...

  return sdk.client
    .fetch<HttpTypes.StoreOrderResponse>(`/store/orders/${id}`, {
      query: {
        fields:
          "*payment_collections.payments,*items,*items.metadata,*items.variant,*items.product,+cart.id",
      },
      // ...
    })
    // ...
}
```

You add `+cart.id` to the `fields` parameter to ensure that the cart ID is included in the order data.

***

## Step 7: Add Link to Request Return Page

Before you test the return request page, you'll add a link to the page from the order details page.

In `src/modules/order/templates/order-details-template.tsx`, add the following imports at the top of the file:

```tsx title="src/modules/order/templates/order-details-template.tsx"
import { Button } from "@medusajs/ui"
import { hasReturnableItems } from "@lib/util/returns"
```

Then, in the `OrderDetailsTemplate` component, add the following variable:

```tsx title="src/modules/order/templates/order-details-template.tsx" highlights={[["6"]]}
const OrderDetailsTemplate: React.FC<OrderDetailsTemplateProps> = ({
  order,
}) => {
  // ...

  const hasReturnableItemsInOrder = hasReturnableItems(order)

  // ...
}
```

You add a variable to check whether the order has any returnable items using the `hasReturnableItems` utility function.

Finally, in the `return` statement, find the following lines that display a button to go back to the orders page:

```tsx title="src/modules/order/templates/order-details-template.tsx"
<LocalizedClientLink
  href="/account/orders"
  className="flex gap-2 items-center text-ui-fg-subtle hover:text-ui-fg-base"
  data-testid="back-to-overview-button"
>
  <XMark /> Back to overview
</LocalizedClientLink>
```

And replace them with the following:

```tsx title="src/modules/order/templates/order-details-template.tsx"
<div className="flex gap-2 items-center">
  {hasReturnableItemsInOrder && (
    <LocalizedClientLink href={`/account/orders/return/${order.id}`}>
      {/* @ts-ignore */}
      <Button variant="secondary" size="small">
        Request Return
      </Button>
    </LocalizedClientLink>
  )}
  <LocalizedClientLink
    href="/account/orders"
    className="flex gap-2 items-center text-ui-fg-subtle hover:text-ui-fg-base"
    data-testid="back-to-overview-button"
  >
    <XMark /> Back to overview
  </LocalizedClientLink>
</div>
```

You add a button that links to the return request page next to the "Back to overview" link. The button is displayed only if the order has returnable items.

***

## Test the Return Request Page

Now that you've added the return request page and the necessary components, you can test the functionality.

First, run the following command from the Medusa application's directory to start the development server:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

And run the following command from the storefront directory to start the storefront development server:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

### Prerequisites

#### Create Return Shipping Options

Before you can test the return request page, you need to create return shipping options in the Medusa Admin. To do that:

1. Open the Medusa Admin in your browser at `localhost:9000/app` and log in.
2. Go to Settings -> Locations & Shipping.
3. Click on "View details" of a location to add the shipping options to.
4. In the Shipping Options section, find the "Return options" subsection and click "Create option".
5. In the form, enter the shipping option's details and price.
6. Click "Save" to create the return shipping option.

You can learn more about creating a shipping option in the [Manage Locations](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations/index.html.md) user guide.

![Form to create a return shipping option in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1758012545/Medusa%20Resources/CleanShot_2025-09-16_at_11.47.47_2x_fx6tan.png)

#### Create Return Reasons

Next, you need to create return reasons in the Medusa Admin. To do that:

1. In the Medusa Admin, go to Settings -> Return Reasons.
2. Click "Create" to add a new return reason.
3. In the form, enter the return reason's label and value.
4. Click "Save" to create the return reason.

You can learn more about creating return reasons in the [Manage Return Reasons](https://docs.medusajs.com/user-guide/settings/return-reasons/index.html.md) user guide.

![Form to create a return reason in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1758012657/Medusa%20Resources/CleanShot_2025-09-16_at_11.50.47_2x_qcrqj6.png)

#### Create Customer Account

Next, you need to create a customer account from the Next.js Starter Storefront. To do that:

1. Go to Account.
2. Click the `Join us` link.
3. Enter the customer's details like email and password.
4. Click "Join" to create the account.

![Form to create a customer account in the Next.js Starter Storefront](https://res.cloudinary.com/dza7lstvk/image/upload/v1758012811/Medusa%20Resources/CleanShot_2025-09-16_at_11.52.58_2x_msuuqx.png)

#### Place an Order

As a logged-in customer, place an order from the Next.js Starter Storefront. You can add an item to the cart, proceed to checkout, and complete the order. You'll see an order confirmation page afterward.

![Order confirmation page in the Next.js Starter Storefront](https://res.cloudinary.com/dza7lstvk/image/upload/v1758012923/Medusa%20Resources/CleanShot_2025-09-16_at_11.55.12_2x_zyat0i.png)

#### Deliver Items in the Order

Finally, you need to deliver the items in the order from the Medusa Admin to make them returnable. To do that:

1. In the Medusa Admin, go to Orders.
2. Click on the order you just placed from the storefront.
3. Under the "Unfulfilled Items" section, click the <InlineIcon Icon={EllipsisHorizontal} alt="three-dots" /> icon and choose "Fulfill Items" from the dropdown.
4. In the form, choose a location, a shipping method, and the quantity to fulfill.
5. Click "Create Fulfillment" to create the fulfillment.
6. In the "Fulfillment #1" section, click the "Mark as delivered" button to mark the fulfillment as delivered.

The order's fulfillment status will change to "Delivered", and the items will become returnable.

Learn more about creating fulfillments and marking them as delivered in the [Manage Order Fulfillments](https://docs.medusajs.com/user-guide/orders/fulfillments/index.html.md) user guide.

![Order details page in the Medusa Admin showing the fulfillment status as "Delivered"](https://res.cloudinary.com/dza7lstvk/image/upload/v1758013188/Medusa%20Resources/CleanShot_2025-09-16_at_11.59.29_2x_tlqaoo.png)

### Test the Return Request Flow

You can now request a return from the storefront as the logged-in customer. To do that:

1. In the storefront, go to Account -> Orders.
2. Click the "See details" button of the order.
3. Click the "Request Return" button to go to the return request page.

If you don't see the "Request Return" button, try hard-refreshing the page or clearing the browser cache.

![Order details page in the Next.js Starter Storefront showing the "Request Return" button](https://res.cloudinary.com/dza7lstvk/image/upload/v1758013344/Medusa%20Resources/CleanShot_2025-09-16_at_12.02.03_2x_ehirsj.png)

4. In the Request Return page, increment the quantity of an item to return, select a return reason, and provide an optional note.
5. Choose a return shipping option.
6. Click the "Request Return" button to submit the return request.

![Request Return page in the Next.js Starter Storefront showing the item and shipping selectors](https://res.cloudinary.com/dza7lstvk/image/upload/v1758013438/Medusa%20Resources/CleanShot_2025-09-16_at_12.03.41_2x_ksmlpt.png)

You should see a success message after submitting the return request.

If you open the order's details page in the Medusa Admin, you'll see the requested return in the Activity section. You'll also see a note below the order's items that were requested for return.

![Order details page in the Medusa Admin showing the requested return in the Activity section](https://res.cloudinary.com/dza7lstvk/image/upload/v1758013557/Medusa%20Resources/CleanShot_2025-09-16_at_12.05.31_2x_wv9z8o.png)

***

## Next Steps

### Handle Return Requests in the Medusa Admin

As a merchant, you can now handle return requests from the Medusa Admin. You can:

1. [Mark Return Items as Received](https://docs.medusajs.com/user-guide/orders/returns#mark-return-items-as-received/index.html.md). This is equivalent to approving the return request. This sets the return's status to "received" and restocks the returned items.

![Form to mark return items as received in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1758013778/Medusa%20Resources/CleanShot_2025-09-16_at_12.09.23_2x_r07tcb.png)

2. If you've previously captured the order's payment and there's an outstanding amount, you can [refund the outstanding amount](https://docs.medusajs.com/user-guide/orders/payments#refund-payment/index.html.md).

![Button to refund the outstanding amount in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1758014212/Medusa%20Resources/CleanShot_2025-09-16_at_12.16.35_2x_ibmkdq.png)

3. [Cancel a return request](https://docs.medusajs.com/user-guide/orders/returns#cancel-requested-return/index.html.md). This sets the return's status to "canceled". The customer can request a new return if needed.

![Button to cancel a return request in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1758013651/Medusa%20Resources/CleanShot_2025-09-16_at_12.07.09_2x_husdak.png)

### Customize the Return Request Page

You can customize the return request page to fit your storefront's design and requirements. You can also change how returns are created. For example, you can let guest customers request returns, set custom prices for return shipping options, or specify a stock location to return the items to.

Refer to the [Create Return API reference](https://docs.medusajs.com/api/api/store#returns_postreturns) to see the available parameters when creating a return request.

### Learn More about Medusa

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.




<H3 className="!mt-0">Just Getting Started?</H3>

Check out the [Medusa v2 Documentation](https://docs.medusajs.com/docs/index.html.md).

<H3 className="!mt-0">Medusa JS SDK</H3>

To use Medusa's JS SDK library, install the following packages in your project (not required for admin customizations):

```bash
npm install @medusajs/js-sdk@latest @medusajs/types@latest
```

Learn more about the JS SDK and how to configure it in [this documentation](https://docs.medusajs.com/resources/js-sdk/index.html.md).

### Download Full Reference

Download this reference as an OpenApi YAML file. You can import this file to tools like Postman and start sending requests directly to your Medusa application.




## Admin API Reference

- [GET /admin/api-keys](https://docs.medusajs.com/api/admin#api-keys_getapikeys)
- [POST /admin/api-keys](https://docs.medusajs.com/api/admin#api-keys_postapikeys)
- [GET /admin/api-keys/{id}](https://docs.medusajs.com/api/admin#api-keys_getapikeysid)
- [POST /admin/api-keys/{id}](https://docs.medusajs.com/api/admin#api-keys_postapikeysid)
- [DELETE /admin/api-keys/{id}](https://docs.medusajs.com/api/admin#api-keys_deleteapikeysid)
- [POST /admin/api-keys/{id}/revoke](https://docs.medusajs.com/api/admin#api-keys_postapikeysidrevoke)
- [POST /admin/api-keys/{id}/sales-channels](https://docs.medusajs.com/api/admin#api-keys_postapikeysidsaleschannels)
- [GET /admin/campaigns](https://docs.medusajs.com/api/admin#campaigns_getcampaigns)
- [POST /admin/campaigns](https://docs.medusajs.com/api/admin#campaigns_postcampaigns)
- [GET /admin/campaigns/{id}](https://docs.medusajs.com/api/admin#campaigns_getcampaignsid)
- [POST /admin/campaigns/{id}](https://docs.medusajs.com/api/admin#campaigns_postcampaignsid)
- [DELETE /admin/campaigns/{id}](https://docs.medusajs.com/api/admin#campaigns_deletecampaignsid)
- [POST /admin/campaigns/{id}/promotions](https://docs.medusajs.com/api/admin#campaigns_postcampaignsidpromotions)
- [GET /admin/claims](https://docs.medusajs.com/api/admin#claims_getclaims)
- [POST /admin/claims](https://docs.medusajs.com/api/admin#claims_postclaims)
- [GET /admin/claims/{id}](https://docs.medusajs.com/api/admin#claims_getclaimsid)
- [POST /admin/claims/{id}/cancel](https://docs.medusajs.com/api/admin#claims_postclaimsidcancel)
- [POST /admin/claims/{id}/claim-items](https://docs.medusajs.com/api/admin#claims_postclaimsidclaimitems)
- [POST /admin/claims/{id}/claim-items/{action_id}](https://docs.medusajs.com/api/admin#claims_postclaimsidclaimitemsaction_id)
- [DELETE /admin/claims/{id}/claim-items/{action_id}](https://docs.medusajs.com/api/admin#claims_deleteclaimsidclaimitemsaction_id)
- [POST /admin/claims/{id}/inbound/items](https://docs.medusajs.com/api/admin#claims_postclaimsidinbounditems)
- [POST /admin/claims/{id}/inbound/items/{action_id}](https://docs.medusajs.com/api/admin#claims_postclaimsidinbounditemsaction_id)
- [DELETE /admin/claims/{id}/inbound/items/{action_id}](https://docs.medusajs.com/api/admin#claims_deleteclaimsidinbounditemsaction_id)
- [POST /admin/claims/{id}/inbound/shipping-method](https://docs.medusajs.com/api/admin#claims_postclaimsidinboundshippingmethod)
- [POST /admin/claims/{id}/inbound/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#claims_postclaimsidinboundshippingmethodaction_id)
- [DELETE /admin/claims/{id}/inbound/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#claims_deleteclaimsidinboundshippingmethodaction_id)
- [POST /admin/claims/{id}/outbound/items](https://docs.medusajs.com/api/admin#claims_postclaimsidoutbounditems)
- [POST /admin/claims/{id}/outbound/items/{action_id}](https://docs.medusajs.com/api/admin#claims_postclaimsidoutbounditemsaction_id)
- [DELETE /admin/claims/{id}/outbound/items/{action_id}](https://docs.medusajs.com/api/admin#claims_deleteclaimsidoutbounditemsaction_id)
- [POST /admin/claims/{id}/outbound/shipping-method](https://docs.medusajs.com/api/admin#claims_postclaimsidoutboundshippingmethod)
- [POST /admin/claims/{id}/outbound/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#claims_postclaimsidoutboundshippingmethodaction_id)
- [DELETE /admin/claims/{id}/outbound/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#claims_deleteclaimsidoutboundshippingmethodaction_id)
- [POST /admin/claims/{id}/request](https://docs.medusajs.com/api/admin#claims_postclaimsidrequest)
- [DELETE /admin/claims/{id}/request](https://docs.medusajs.com/api/admin#claims_deleteclaimsidrequest)
- [GET /admin/collections](https://docs.medusajs.com/api/admin#collections_getcollections)
- [POST /admin/collections](https://docs.medusajs.com/api/admin#collections_postcollections)
- [GET /admin/collections/{id}](https://docs.medusajs.com/api/admin#collections_getcollectionsid)
- [POST /admin/collections/{id}](https://docs.medusajs.com/api/admin#collections_postcollectionsid)
- [DELETE /admin/collections/{id}](https://docs.medusajs.com/api/admin#collections_deletecollectionsid)
- [POST /admin/collections/{id}/products](https://docs.medusajs.com/api/admin#collections_postcollectionsidproducts)
- [GET /admin/currencies](https://docs.medusajs.com/api/admin#currencies_getcurrencies)
- [GET /admin/currencies/{code}](https://docs.medusajs.com/api/admin#currencies_getcurrenciescode)
- [GET /admin/customer-groups](https://docs.medusajs.com/api/admin#customer-groups_getcustomergroups)
- [POST /admin/customer-groups](https://docs.medusajs.com/api/admin#customer-groups_postcustomergroups)
- [GET /admin/customer-groups/{id}](https://docs.medusajs.com/api/admin#customer-groups_getcustomergroupsid)
- [POST /admin/customer-groups/{id}](https://docs.medusajs.com/api/admin#customer-groups_postcustomergroupsid)
- [DELETE /admin/customer-groups/{id}](https://docs.medusajs.com/api/admin#customer-groups_deletecustomergroupsid)
- [POST /admin/customer-groups/{id}/customers](https://docs.medusajs.com/api/admin#customer-groups_postcustomergroupsidcustomers)
- [GET /admin/customers](https://docs.medusajs.com/api/admin#customers_getcustomers)
- [POST /admin/customers](https://docs.medusajs.com/api/admin#customers_postcustomers)
- [GET /admin/customers/{id}](https://docs.medusajs.com/api/admin#customers_getcustomersid)
- [POST /admin/customers/{id}](https://docs.medusajs.com/api/admin#customers_postcustomersid)
- [DELETE /admin/customers/{id}](https://docs.medusajs.com/api/admin#customers_deletecustomersid)
- [GET /admin/customers/{id}/addresses](https://docs.medusajs.com/api/admin#customers_getcustomersidaddresses)
- [POST /admin/customers/{id}/addresses](https://docs.medusajs.com/api/admin#customers_postcustomersidaddresses)
- [GET /admin/customers/{id}/addresses/{address_id}](https://docs.medusajs.com/api/admin#customers_getcustomersidaddressesaddress_id)
- [POST /admin/customers/{id}/addresses/{address_id}](https://docs.medusajs.com/api/admin#customers_postcustomersidaddressesaddress_id)
- [DELETE /admin/customers/{id}/addresses/{address_id}](https://docs.medusajs.com/api/admin#customers_deletecustomersidaddressesaddress_id)
- [POST /admin/customers/{id}/customer-groups](https://docs.medusajs.com/api/admin#customers_postcustomersidcustomergroups)
- [GET /admin/draft-orders](https://docs.medusajs.com/api/admin#draft-orders_getdraftorders)
- [POST /admin/draft-orders](https://docs.medusajs.com/api/admin#draft-orders_postdraftorders)
- [GET /admin/draft-orders/{id}](https://docs.medusajs.com/api/admin#draft-orders_getdraftordersid)
- [POST /admin/draft-orders/{id}](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersid)
- [DELETE /admin/draft-orders/{id}](https://docs.medusajs.com/api/admin#draft-orders_deletedraftordersid)
- [POST /admin/draft-orders/{id}/convert-to-order](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersidconverttoorder)
- [POST /admin/draft-orders/{id}/edit](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersidedit)
- [DELETE /admin/draft-orders/{id}/edit](https://docs.medusajs.com/api/admin#draft-orders_deletedraftordersidedit)
- [POST /admin/draft-orders/{id}/edit/confirm](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersideditconfirm)
- [POST /admin/draft-orders/{id}/edit/items](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersidedititems)
- [POST /admin/draft-orders/{id}/edit/items/item/{item_id}](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersidedititemsitemitem_id)
- [POST /admin/draft-orders/{id}/edit/items/{action_id}](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersidedititemsaction_id)
- [DELETE /admin/draft-orders/{id}/edit/items/{action_id}](https://docs.medusajs.com/api/admin#draft-orders_deletedraftordersidedititemsaction_id)
- [POST /admin/draft-orders/{id}/edit/promotions](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersideditpromotions)
- [DELETE /admin/draft-orders/{id}/edit/promotions](https://docs.medusajs.com/api/admin#draft-orders_deletedraftordersideditpromotions)
- [POST /admin/draft-orders/{id}/edit/request](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersideditrequest)
- [POST /admin/draft-orders/{id}/edit/shipping-methods](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersideditshippingmethods)
- [POST /admin/draft-orders/{id}/edit/shipping-methods/method/{method_id}](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersideditshippingmethodsmethodmethod_id)
- [DELETE /admin/draft-orders/{id}/edit/shipping-methods/method/{method_id}](https://docs.medusajs.com/api/admin#draft-orders_deletedraftordersideditshippingmethodsmethodmethod_id)
- [POST /admin/draft-orders/{id}/edit/shipping-methods/{action_id}](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersideditshippingmethodsaction_id)
- [DELETE /admin/draft-orders/{id}/edit/shipping-methods/{action_id}](https://docs.medusajs.com/api/admin#draft-orders_deletedraftordersideditshippingmethodsaction_id)
- [GET /admin/exchanges](https://docs.medusajs.com/api/admin#exchanges_getexchanges)
- [POST /admin/exchanges](https://docs.medusajs.com/api/admin#exchanges_postexchanges)
- [GET /admin/exchanges/{id}](https://docs.medusajs.com/api/admin#exchanges_getexchangesid)
- [POST /admin/exchanges/{id}/cancel](https://docs.medusajs.com/api/admin#exchanges_postexchangesidcancel)
- [POST /admin/exchanges/{id}/inbound/items](https://docs.medusajs.com/api/admin#exchanges_postexchangesidinbounditems)
- [POST /admin/exchanges/{id}/inbound/items/{action_id}](https://docs.medusajs.com/api/admin#exchanges_postexchangesidinbounditemsaction_id)
- [DELETE /admin/exchanges/{id}/inbound/items/{action_id}](https://docs.medusajs.com/api/admin#exchanges_deleteexchangesidinbounditemsaction_id)
- [POST /admin/exchanges/{id}/inbound/shipping-method](https://docs.medusajs.com/api/admin#exchanges_postexchangesidinboundshippingmethod)
- [POST /admin/exchanges/{id}/inbound/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#exchanges_postexchangesidinboundshippingmethodaction_id)
- [DELETE /admin/exchanges/{id}/inbound/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#exchanges_deleteexchangesidinboundshippingmethodaction_id)
- [POST /admin/exchanges/{id}/outbound/items](https://docs.medusajs.com/api/admin#exchanges_postexchangesidoutbounditems)
- [POST /admin/exchanges/{id}/outbound/items/{action_id}](https://docs.medusajs.com/api/admin#exchanges_postexchangesidoutbounditemsaction_id)
- [DELETE /admin/exchanges/{id}/outbound/items/{action_id}](https://docs.medusajs.com/api/admin#exchanges_deleteexchangesidoutbounditemsaction_id)
- [POST /admin/exchanges/{id}/outbound/shipping-method](https://docs.medusajs.com/api/admin#exchanges_postexchangesidoutboundshippingmethod)
- [POST /admin/exchanges/{id}/outbound/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#exchanges_postexchangesidoutboundshippingmethodaction_id)
- [DELETE /admin/exchanges/{id}/outbound/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#exchanges_deleteexchangesidoutboundshippingmethodaction_id)
- [POST /admin/exchanges/{id}/request](https://docs.medusajs.com/api/admin#exchanges_postexchangesidrequest)
- [DELETE /admin/exchanges/{id}/request](https://docs.medusajs.com/api/admin#exchanges_deleteexchangesidrequest)
- [GET /admin/feature-flags](https://docs.medusajs.com/api/admin#feature-flags_getfeatureflags)
- [GET /admin/fulfillment-providers](https://docs.medusajs.com/api/admin#fulfillment-providers_getfulfillmentproviders)
- [GET /admin/fulfillment-providers/{id}/options](https://docs.medusajs.com/api/admin#fulfillment-providers_getfulfillmentprovidersidoptions)
- [DELETE /admin/fulfillment-sets/{id}](https://docs.medusajs.com/api/admin#fulfillment-sets_deletefulfillmentsetsid)
- [POST /admin/fulfillment-sets/{id}/service-zones](https://docs.medusajs.com/api/admin#fulfillment-sets_postfulfillmentsetsidservicezones)
- [GET /admin/fulfillment-sets/{id}/service-zones/{zone_id}](https://docs.medusajs.com/api/admin#fulfillment-sets_getfulfillmentsetsidservicezoneszone_id)
- [POST /admin/fulfillment-sets/{id}/service-zones/{zone_id}](https://docs.medusajs.com/api/admin#fulfillment-sets_postfulfillmentsetsidservicezoneszone_id)
- [DELETE /admin/fulfillment-sets/{id}/service-zones/{zone_id}](https://docs.medusajs.com/api/admin#fulfillment-sets_deletefulfillmentsetsidservicezoneszone_id)
- [POST /admin/fulfillments](https://docs.medusajs.com/api/admin#fulfillments_postfulfillments)
- [POST /admin/fulfillments/{id}/cancel](https://docs.medusajs.com/api/admin#fulfillments_postfulfillmentsidcancel)
- [POST /admin/fulfillments/{id}/shipment](https://docs.medusajs.com/api/admin#fulfillments_postfulfillmentsidshipment)
- [GET /admin/gift-cards](https://docs.medusajs.com/api/admin#gift-cards_getgiftcards)
- [POST /admin/gift-cards](https://docs.medusajs.com/api/admin#gift-cards_postgiftcards)
- [GET /admin/gift-cards/{id}](https://docs.medusajs.com/api/admin#gift-cards_getgiftcardsid)
- [POST /admin/gift-cards/{id}](https://docs.medusajs.com/api/admin#gift-cards_postgiftcardsid)
- [GET /admin/gift-cards/{id}/orders](https://docs.medusajs.com/api/admin#gift-cards_getgiftcardsidorders)
- [POST /admin/gift-cards/{id}/redeem](https://docs.medusajs.com/api/admin#gift-cards_postgiftcardsidredeem)
- [GET /admin/index/details](https://docs.medusajs.com/api/admin#index_getindexdetails)
- [POST /admin/index/sync](https://docs.medusajs.com/api/admin#index_postindexsync)
- [GET /admin/inventory-items](https://docs.medusajs.com/api/admin#inventory-items_getinventoryitems)
- [POST /admin/inventory-items](https://docs.medusajs.com/api/admin#inventory-items_postinventoryitems)
- [POST /admin/inventory-items/location-levels/batch](https://docs.medusajs.com/api/admin#inventory-items_postinventoryitemslocationlevelsbatch)
- [GET /admin/inventory-items/{id}](https://docs.medusajs.com/api/admin#inventory-items_getinventoryitemsid)
- [POST /admin/inventory-items/{id}](https://docs.medusajs.com/api/admin#inventory-items_postinventoryitemsid)
- [DELETE /admin/inventory-items/{id}](https://docs.medusajs.com/api/admin#inventory-items_deleteinventoryitemsid)
- [GET /admin/inventory-items/{id}/location-levels](https://docs.medusajs.com/api/admin#inventory-items_getinventoryitemsidlocationlevels)
- [POST /admin/inventory-items/{id}/location-levels](https://docs.medusajs.com/api/admin#inventory-items_postinventoryitemsidlocationlevels)
- [POST /admin/inventory-items/{id}/location-levels/batch](https://docs.medusajs.com/api/admin#inventory-items_postinventoryitemsidlocationlevelsbatch)
- [POST /admin/inventory-items/{id}/location-levels/{location_id}](https://docs.medusajs.com/api/admin#inventory-items_postinventoryitemsidlocationlevelslocation_id)
- [DELETE /admin/inventory-items/{id}/location-levels/{location_id}](https://docs.medusajs.com/api/admin#inventory-items_deleteinventoryitemsidlocationlevelslocation_id)
- [GET /admin/invites](https://docs.medusajs.com/api/admin#invites_getinvites)
- [POST /admin/invites](https://docs.medusajs.com/api/admin#invites_postinvites)
- [POST /admin/invites/accept](https://docs.medusajs.com/api/admin#invites_postinvitesaccept)
- [GET /admin/invites/{id}](https://docs.medusajs.com/api/admin#invites_getinvitesid)
- [DELETE /admin/invites/{id}](https://docs.medusajs.com/api/admin#invites_deleteinvitesid)
- [POST /admin/invites/{id}/resend](https://docs.medusajs.com/api/admin#invites_postinvitesidresend)
- [GET /admin/locales](https://docs.medusajs.com/api/admin#locales_getlocales)
- [GET /admin/locales/{code}](https://docs.medusajs.com/api/admin#locales_getlocalescode)
- [GET /admin/notifications](https://docs.medusajs.com/api/admin#notifications_getnotifications)
- [GET /admin/notifications/{id}](https://docs.medusajs.com/api/admin#notifications_getnotificationsid)
- [POST /admin/order-changes/{id}](https://docs.medusajs.com/api/admin#order-changes_postorderchangesid)
- [POST /admin/order-edits](https://docs.medusajs.com/api/admin#order-edits_postorderedits)
- [DELETE /admin/order-edits/{id}](https://docs.medusajs.com/api/admin#order-edits_deleteordereditsid)
- [POST /admin/order-edits/{id}/confirm](https://docs.medusajs.com/api/admin#order-edits_postordereditsidconfirm)
- [POST /admin/order-edits/{id}/items](https://docs.medusajs.com/api/admin#order-edits_postordereditsiditems)
- [POST /admin/order-edits/{id}/items/item/{item_id}](https://docs.medusajs.com/api/admin#order-edits_postordereditsiditemsitemitem_id)
- [POST /admin/order-edits/{id}/items/{action_id}](https://docs.medusajs.com/api/admin#order-edits_postordereditsiditemsaction_id)
- [DELETE /admin/order-edits/{id}/items/{action_id}](https://docs.medusajs.com/api/admin#order-edits_deleteordereditsiditemsaction_id)
- [POST /admin/order-edits/{id}/request](https://docs.medusajs.com/api/admin#order-edits_postordereditsidrequest)
- [POST /admin/order-edits/{id}/shipping-method](https://docs.medusajs.com/api/admin#order-edits_postordereditsidshippingmethod)
- [POST /admin/order-edits/{id}/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#order-edits_postordereditsidshippingmethodaction_id)
- [DELETE /admin/order-edits/{id}/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#order-edits_deleteordereditsidshippingmethodaction_id)
- [GET /admin/orders](https://docs.medusajs.com/api/admin#orders_getorders)
- [POST /admin/orders/export](https://docs.medusajs.com/api/admin#orders_postordersexport)
- [GET /admin/orders/{id}](https://docs.medusajs.com/api/admin#orders_getordersid)
- [POST /admin/orders/{id}](https://docs.medusajs.com/api/admin#orders_postordersid)
- [POST /admin/orders/{id}/archive](https://docs.medusajs.com/api/admin#orders_postordersidarchive)
- [POST /admin/orders/{id}/cancel](https://docs.medusajs.com/api/admin#orders_postordersidcancel)
- [GET /admin/orders/{id}/changes](https://docs.medusajs.com/api/admin#orders_getordersidchanges)
- [POST /admin/orders/{id}/complete](https://docs.medusajs.com/api/admin#orders_postordersidcomplete)
- [POST /admin/orders/{id}/credit-lines](https://docs.medusajs.com/api/admin#orders_postordersidcreditlines)
- [POST /admin/orders/{id}/fulfillments](https://docs.medusajs.com/api/admin#orders_postordersidfulfillments)
- [POST /admin/orders/{id}/fulfillments/{fulfillment_id}/cancel](https://docs.medusajs.com/api/admin#orders_postordersidfulfillmentsfulfillment_idcancel)
- [POST /admin/orders/{id}/fulfillments/{fulfillment_id}/mark-as-delivered](https://docs.medusajs.com/api/admin#orders_postordersidfulfillmentsfulfillment_idmarkasdelivered)
- [POST /admin/orders/{id}/fulfillments/{fulfillment_id}/shipments](https://docs.medusajs.com/api/admin#orders_postordersidfulfillmentsfulfillment_idshipments)
- [GET /admin/orders/{id}/line-items](https://docs.medusajs.com/api/admin#orders_getordersidlineitems)
- [GET /admin/orders/{id}/preview](https://docs.medusajs.com/api/admin#orders_getordersidpreview)
- [GET /admin/orders/{id}/shipping-options](https://docs.medusajs.com/api/admin#orders_getordersidshippingoptions)
- [POST /admin/orders/{id}/transfer](https://docs.medusajs.com/api/admin#orders_postordersidtransfer)
- [POST /admin/orders/{id}/transfer/cancel](https://docs.medusajs.com/api/admin#orders_postordersidtransfercancel)
- [POST /admin/payment-collections](https://docs.medusajs.com/api/admin#payment-collections_postpaymentcollections)
- [DELETE /admin/payment-collections/{id}](https://docs.medusajs.com/api/admin#payment-collections_deletepaymentcollectionsid)
- [POST /admin/payment-collections/{id}/mark-as-paid](https://docs.medusajs.com/api/admin#payment-collections_postpaymentcollectionsidmarkaspaid)
- [GET /admin/payments](https://docs.medusajs.com/api/admin#payments_getpayments)
- [GET /admin/payments/payment-providers](https://docs.medusajs.com/api/admin#payments_getpaymentspaymentproviders)
- [GET /admin/payments/{id}](https://docs.medusajs.com/api/admin#payments_getpaymentsid)
- [POST /admin/payments/{id}/capture](https://docs.medusajs.com/api/admin#payments_postpaymentsidcapture)
- [POST /admin/payments/{id}/refund](https://docs.medusajs.com/api/admin#payments_postpaymentsidrefund)
- [GET /admin/plugins](https://docs.medusajs.com/api/admin#plugins_getplugins)
- [GET /admin/price-lists](https://docs.medusajs.com/api/admin#price-lists_getpricelists)
- [POST /admin/price-lists](https://docs.medusajs.com/api/admin#price-lists_postpricelists)
- [GET /admin/price-lists/{id}](https://docs.medusajs.com/api/admin#price-lists_getpricelistsid)
- [POST /admin/price-lists/{id}](https://docs.medusajs.com/api/admin#price-lists_postpricelistsid)
- [DELETE /admin/price-lists/{id}](https://docs.medusajs.com/api/admin#price-lists_deletepricelistsid)
- [GET /admin/price-lists/{id}/prices](https://docs.medusajs.com/api/admin#price-lists_getpricelistsidprices)
- [POST /admin/price-lists/{id}/prices/batch](https://docs.medusajs.com/api/admin#price-lists_postpricelistsidpricesbatch)
- [POST /admin/price-lists/{id}/products](https://docs.medusajs.com/api/admin#price-lists_postpricelistsidproducts)
- [GET /admin/price-preferences](https://docs.medusajs.com/api/admin#price-preferences_getpricepreferences)
- [POST /admin/price-preferences](https://docs.medusajs.com/api/admin#price-preferences_postpricepreferences)
- [GET /admin/price-preferences/{id}](https://docs.medusajs.com/api/admin#price-preferences_getpricepreferencesid)
- [POST /admin/price-preferences/{id}](https://docs.medusajs.com/api/admin#price-preferences_postpricepreferencesid)
- [DELETE /admin/price-preferences/{id}](https://docs.medusajs.com/api/admin#price-preferences_deletepricepreferencesid)
- [GET /admin/product-categories](https://docs.medusajs.com/api/admin#product-categories_getproductcategories)
- [POST /admin/product-categories](https://docs.medusajs.com/api/admin#product-categories_postproductcategories)
- [GET /admin/product-categories/{id}](https://docs.medusajs.com/api/admin#product-categories_getproductcategoriesid)
- [POST /admin/product-categories/{id}](https://docs.medusajs.com/api/admin#product-categories_postproductcategoriesid)
- [DELETE /admin/product-categories/{id}](https://docs.medusajs.com/api/admin#product-categories_deleteproductcategoriesid)
- [POST /admin/product-categories/{id}/products](https://docs.medusajs.com/api/admin#product-categories_postproductcategoriesidproducts)
- [GET /admin/product-tags](https://docs.medusajs.com/api/admin#product-tags_getproducttags)
- [POST /admin/product-tags](https://docs.medusajs.com/api/admin#product-tags_postproducttags)
- [GET /admin/product-tags/{id}](https://docs.medusajs.com/api/admin#product-tags_getproducttagsid)
- [POST /admin/product-tags/{id}](https://docs.medusajs.com/api/admin#product-tags_postproducttagsid)
- [DELETE /admin/product-tags/{id}](https://docs.medusajs.com/api/admin#product-tags_deleteproducttagsid)
- [GET /admin/product-types](https://docs.medusajs.com/api/admin#product-types_getproducttypes)
- [POST /admin/product-types](https://docs.medusajs.com/api/admin#product-types_postproducttypes)
- [GET /admin/product-types/{id}](https://docs.medusajs.com/api/admin#product-types_getproducttypesid)
- [POST /admin/product-types/{id}](https://docs.medusajs.com/api/admin#product-types_postproducttypesid)
- [DELETE /admin/product-types/{id}](https://docs.medusajs.com/api/admin#product-types_deleteproducttypesid)
- [GET /admin/product-variants](https://docs.medusajs.com/api/admin#product-variants_getproductvariants)
- [GET /admin/products](https://docs.medusajs.com/api/admin#products_getproducts)
- [POST /admin/products](https://docs.medusajs.com/api/admin#products_postproducts)
- [POST /admin/products/batch](https://docs.medusajs.com/api/admin#products_postproductsbatch)
- [POST /admin/products/export](https://docs.medusajs.com/api/admin#products_postproductsexport)
- [POST /admin/products/import](https://docs.medusajs.com/api/admin#products_postproductsimport)
- [POST /admin/products/import/{transaction_id}/confirm](https://docs.medusajs.com/api/admin#products_postproductsimporttransaction_idconfirm)
- [POST /admin/products/imports](https://docs.medusajs.com/api/admin#products_postproductsimports)
- [POST /admin/products/imports/{transaction_id}/confirm](https://docs.medusajs.com/api/admin#products_postproductsimportstransaction_idconfirm)
- [GET /admin/products/{id}](https://docs.medusajs.com/api/admin#products_getproductsid)
- [POST /admin/products/{id}](https://docs.medusajs.com/api/admin#products_postproductsid)
- [DELETE /admin/products/{id}](https://docs.medusajs.com/api/admin#products_deleteproductsid)
- [POST /admin/products/{id}/images/{image_id}/variants/batch](https://docs.medusajs.com/api/admin#products_postproductsidimagesimage_idvariantsbatch)
- [GET /admin/products/{id}/options](https://docs.medusajs.com/api/admin#products_getproductsidoptions)
- [POST /admin/products/{id}/options](https://docs.medusajs.com/api/admin#products_postproductsidoptions)
- [GET /admin/products/{id}/options/{option_id}](https://docs.medusajs.com/api/admin#products_getproductsidoptionsoption_id)
- [POST /admin/products/{id}/options/{option_id}](https://docs.medusajs.com/api/admin#products_postproductsidoptionsoption_id)
- [DELETE /admin/products/{id}/options/{option_id}](https://docs.medusajs.com/api/admin#products_deleteproductsidoptionsoption_id)
- [GET /admin/products/{id}/variants](https://docs.medusajs.com/api/admin#products_getproductsidvariants)
- [POST /admin/products/{id}/variants](https://docs.medusajs.com/api/admin#products_postproductsidvariants)
- [POST /admin/products/{id}/variants/batch](https://docs.medusajs.com/api/admin#products_postproductsidvariantsbatch)
- [POST /admin/products/{id}/variants/inventory-items/batch](https://docs.medusajs.com/api/admin#products_postproductsidvariantsinventoryitemsbatch)
- [GET /admin/products/{id}/variants/{variant_id}](https://docs.medusajs.com/api/admin#products_getproductsidvariantsvariant_id)
- [POST /admin/products/{id}/variants/{variant_id}](https://docs.medusajs.com/api/admin#products_postproductsidvariantsvariant_id)
- [DELETE /admin/products/{id}/variants/{variant_id}](https://docs.medusajs.com/api/admin#products_deleteproductsidvariantsvariant_id)
- [POST /admin/products/{id}/variants/{variant_id}/images/batch](https://docs.medusajs.com/api/admin#products_postproductsidvariantsvariant_idimagesbatch)
- [POST /admin/products/{id}/variants/{variant_id}/inventory-items](https://docs.medusajs.com/api/admin#products_postproductsidvariantsvariant_idinventoryitems)
- [POST /admin/products/{id}/variants/{variant_id}/inventory-items/{inventory_item_id}](https://docs.medusajs.com/api/admin#products_postproductsidvariantsvariant_idinventoryitemsinventory_item_id)
- [DELETE /admin/products/{id}/variants/{variant_id}/inventory-items/{inventory_item_id}](https://docs.medusajs.com/api/admin#products_deleteproductsidvariantsvariant_idinventoryitemsinventory_item_id)
- [GET /admin/promotions](https://docs.medusajs.com/api/admin#promotions_getpromotions)
- [POST /admin/promotions](https://docs.medusajs.com/api/admin#promotions_postpromotions)
- [GET /admin/promotions/rule-attribute-options/{rule_type}](https://docs.medusajs.com/api/admin#promotions_getpromotionsruleattributeoptionsrule_type)
- [GET /admin/promotions/rule-value-options/{rule_type}/{rule_attribute_id}](https://docs.medusajs.com/api/admin#promotions_getpromotionsrulevalueoptionsrule_typerule_attribute_id)
- [GET /admin/promotions/{id}](https://docs.medusajs.com/api/admin#promotions_getpromotionsid)
- [POST /admin/promotions/{id}](https://docs.medusajs.com/api/admin#promotions_postpromotionsid)
- [DELETE /admin/promotions/{id}](https://docs.medusajs.com/api/admin#promotions_deletepromotionsid)
- [POST /admin/promotions/{id}/buy-rules/batch](https://docs.medusajs.com/api/admin#promotions_postpromotionsidbuyrulesbatch)
- [POST /admin/promotions/{id}/rules/batch](https://docs.medusajs.com/api/admin#promotions_postpromotionsidrulesbatch)
- [POST /admin/promotions/{id}/target-rules/batch](https://docs.medusajs.com/api/admin#promotions_postpromotionsidtargetrulesbatch)
- [GET /admin/promotions/{id}/{rule_type}](https://docs.medusajs.com/api/admin#promotions_getpromotionsidrule_type)
- [GET /admin/refund-reasons](https://docs.medusajs.com/api/admin#refund-reasons_getrefundreasons)
- [POST /admin/refund-reasons](https://docs.medusajs.com/api/admin#refund-reasons_postrefundreasons)
- [GET /admin/refund-reasons/{id}](https://docs.medusajs.com/api/admin#refund-reasons_getrefundreasonsid)
- [POST /admin/refund-reasons/{id}](https://docs.medusajs.com/api/admin#refund-reasons_postrefundreasonsid)
- [DELETE /admin/refund-reasons/{id}](https://docs.medusajs.com/api/admin#refund-reasons_deleterefundreasonsid)
- [GET /admin/regions](https://docs.medusajs.com/api/admin#regions_getregions)
- [POST /admin/regions](https://docs.medusajs.com/api/admin#regions_postregions)
- [GET /admin/regions/{id}](https://docs.medusajs.com/api/admin#regions_getregionsid)
- [POST /admin/regions/{id}](https://docs.medusajs.com/api/admin#regions_postregionsid)
- [DELETE /admin/regions/{id}](https://docs.medusajs.com/api/admin#regions_deleteregionsid)
- [GET /admin/reservations](https://docs.medusajs.com/api/admin#reservations_getreservations)
- [POST /admin/reservations](https://docs.medusajs.com/api/admin#reservations_postreservations)
- [GET /admin/reservations/{id}](https://docs.medusajs.com/api/admin#reservations_getreservationsid)
- [POST /admin/reservations/{id}](https://docs.medusajs.com/api/admin#reservations_postreservationsid)
- [DELETE /admin/reservations/{id}](https://docs.medusajs.com/api/admin#reservations_deletereservationsid)
- [GET /admin/return-reasons](https://docs.medusajs.com/api/admin#return-reasons_getreturnreasons)
- [POST /admin/return-reasons](https://docs.medusajs.com/api/admin#return-reasons_postreturnreasons)
- [GET /admin/return-reasons/{id}](https://docs.medusajs.com/api/admin#return-reasons_getreturnreasonsid)
- [POST /admin/return-reasons/{id}](https://docs.medusajs.com/api/admin#return-reasons_postreturnreasonsid)
- [DELETE /admin/return-reasons/{id}](https://docs.medusajs.com/api/admin#return-reasons_deletereturnreasonsid)
- [GET /admin/returns](https://docs.medusajs.com/api/admin#returns_getreturns)
- [POST /admin/returns](https://docs.medusajs.com/api/admin#returns_postreturns)
- [GET /admin/returns/{id}](https://docs.medusajs.com/api/admin#returns_getreturnsid)
- [POST /admin/returns/{id}](https://docs.medusajs.com/api/admin#returns_postreturnsid)
- [POST /admin/returns/{id}/cancel](https://docs.medusajs.com/api/admin#returns_postreturnsidcancel)
- [POST /admin/returns/{id}/dismiss-items](https://docs.medusajs.com/api/admin#returns_postreturnsiddismissitems)
- [POST /admin/returns/{id}/dismiss-items/{action_id}](https://docs.medusajs.com/api/admin#returns_postreturnsiddismissitemsaction_id)
- [DELETE /admin/returns/{id}/dismiss-items/{action_id}](https://docs.medusajs.com/api/admin#returns_deletereturnsiddismissitemsaction_id)
- [POST /admin/returns/{id}/receive-items](https://docs.medusajs.com/api/admin#returns_postreturnsidreceiveitems)
- [POST /admin/returns/{id}/receive-items/{action_id}](https://docs.medusajs.com/api/admin#returns_postreturnsidreceiveitemsaction_id)
- [DELETE /admin/returns/{id}/receive-items/{action_id}](https://docs.medusajs.com/api/admin#returns_deletereturnsidreceiveitemsaction_id)
- [POST /admin/returns/{id}/receive](https://docs.medusajs.com/api/admin#returns_postreturnsidreceive)
- [DELETE /admin/returns/{id}/receive](https://docs.medusajs.com/api/admin#returns_deletereturnsidreceive)
- [POST /admin/returns/{id}/receive/confirm](https://docs.medusajs.com/api/admin#returns_postreturnsidreceiveconfirm)
- [POST /admin/returns/{id}/request-items](https://docs.medusajs.com/api/admin#returns_postreturnsidrequestitems)
- [POST /admin/returns/{id}/request-items/{action_id}](https://docs.medusajs.com/api/admin#returns_postreturnsidrequestitemsaction_id)
- [DELETE /admin/returns/{id}/request-items/{action_id}](https://docs.medusajs.com/api/admin#returns_deletereturnsidrequestitemsaction_id)
- [POST /admin/returns/{id}/request](https://docs.medusajs.com/api/admin#returns_postreturnsidrequest)
- [DELETE /admin/returns/{id}/request](https://docs.medusajs.com/api/admin#returns_deletereturnsidrequest)
- [POST /admin/returns/{id}/shipping-method](https://docs.medusajs.com/api/admin#returns_postreturnsidshippingmethod)
- [POST /admin/returns/{id}/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#returns_postreturnsidshippingmethodaction_id)
- [DELETE /admin/returns/{id}/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#returns_deletereturnsidshippingmethodaction_id)
- [GET /admin/sales-channels](https://docs.medusajs.com/api/admin#sales-channels_getsaleschannels)
- [POST /admin/sales-channels](https://docs.medusajs.com/api/admin#sales-channels_postsaleschannels)
- [GET /admin/sales-channels/{id}](https://docs.medusajs.com/api/admin#sales-channels_getsaleschannelsid)
- [POST /admin/sales-channels/{id}](https://docs.medusajs.com/api/admin#sales-channels_postsaleschannelsid)
- [DELETE /admin/sales-channels/{id}](https://docs.medusajs.com/api/admin#sales-channels_deletesaleschannelsid)
- [POST /admin/sales-channels/{id}/products](https://docs.medusajs.com/api/admin#sales-channels_postsaleschannelsidproducts)
- [GET /admin/shipping-option-types](https://docs.medusajs.com/api/admin#shipping-option-types_getshippingoptiontypes)
- [POST /admin/shipping-option-types](https://docs.medusajs.com/api/admin#shipping-option-types_postshippingoptiontypes)
- [GET /admin/shipping-option-types/{id}](https://docs.medusajs.com/api/admin#shipping-option-types_getshippingoptiontypesid)
- [POST /admin/shipping-option-types/{id}](https://docs.medusajs.com/api/admin#shipping-option-types_postshippingoptiontypesid)
- [DELETE /admin/shipping-option-types/{id}](https://docs.medusajs.com/api/admin#shipping-option-types_deleteshippingoptiontypesid)
- [GET /admin/shipping-options](https://docs.medusajs.com/api/admin#shipping-options_getshippingoptions)
- [POST /admin/shipping-options](https://docs.medusajs.com/api/admin#shipping-options_postshippingoptions)
- [GET /admin/shipping-options/{id}](https://docs.medusajs.com/api/admin#shipping-options_getshippingoptionsid)
- [POST /admin/shipping-options/{id}](https://docs.medusajs.com/api/admin#shipping-options_postshippingoptionsid)
- [DELETE /admin/shipping-options/{id}](https://docs.medusajs.com/api/admin#shipping-options_deleteshippingoptionsid)
- [POST /admin/shipping-options/{id}/rules/batch](https://docs.medusajs.com/api/admin#shipping-options_postshippingoptionsidrulesbatch)
- [GET /admin/shipping-profiles](https://docs.medusajs.com/api/admin#shipping-profiles_getshippingprofiles)
- [POST /admin/shipping-profiles](https://docs.medusajs.com/api/admin#shipping-profiles_postshippingprofiles)
- [GET /admin/shipping-profiles/{id}](https://docs.medusajs.com/api/admin#shipping-profiles_getshippingprofilesid)
- [POST /admin/shipping-profiles/{id}](https://docs.medusajs.com/api/admin#shipping-profiles_postshippingprofilesid)
- [DELETE /admin/shipping-profiles/{id}](https://docs.medusajs.com/api/admin#shipping-profiles_deleteshippingprofilesid)
- [GET /admin/stock-locations](https://docs.medusajs.com/api/admin#stock-locations_getstocklocations)
- [POST /admin/stock-locations](https://docs.medusajs.com/api/admin#stock-locations_poststocklocations)
- [GET /admin/stock-locations/{id}](https://docs.medusajs.com/api/admin#stock-locations_getstocklocationsid)
- [POST /admin/stock-locations/{id}](https://docs.medusajs.com/api/admin#stock-locations_poststocklocationsid)
- [DELETE /admin/stock-locations/{id}](https://docs.medusajs.com/api/admin#stock-locations_deletestocklocationsid)
- [POST /admin/stock-locations/{id}/fulfillment-providers](https://docs.medusajs.com/api/admin#stock-locations_poststocklocationsidfulfillmentproviders)
- [POST /admin/stock-locations/{id}/fulfillment-sets](https://docs.medusajs.com/api/admin#stock-locations_poststocklocationsidfulfillmentsets)
- [POST /admin/stock-locations/{id}/sales-channels](https://docs.medusajs.com/api/admin#stock-locations_poststocklocationsidsaleschannels)
- [GET /admin/store-credit-accounts](https://docs.medusajs.com/api/admin#store-credit-accounts_getstorecreditaccounts)
- [POST /admin/store-credit-accounts](https://docs.medusajs.com/api/admin#store-credit-accounts_poststorecreditaccounts)
- [GET /admin/store-credit-accounts/{id}](https://docs.medusajs.com/api/admin#store-credit-accounts_getstorecreditaccountsid)
- [GET /admin/store-credit-accounts/{id}/transactions](https://docs.medusajs.com/api/admin#store-credit-accounts_getstorecreditaccountsidtransactions)
- [GET /admin/stores](https://docs.medusajs.com/api/admin#stores_getstores)
- [GET /admin/stores/{id}](https://docs.medusajs.com/api/admin#stores_getstoresid)
- [POST /admin/stores/{id}](https://docs.medusajs.com/api/admin#stores_poststoresid)
- [GET /admin/tax-providers](https://docs.medusajs.com/api/admin#tax-providers_gettaxproviders)
- [GET /admin/tax-rates](https://docs.medusajs.com/api/admin#tax-rates_gettaxrates)
- [POST /admin/tax-rates](https://docs.medusajs.com/api/admin#tax-rates_posttaxrates)
- [GET /admin/tax-rates/{id}](https://docs.medusajs.com/api/admin#tax-rates_gettaxratesid)
- [POST /admin/tax-rates/{id}](https://docs.medusajs.com/api/admin#tax-rates_posttaxratesid)
- [DELETE /admin/tax-rates/{id}](https://docs.medusajs.com/api/admin#tax-rates_deletetaxratesid)
- [POST /admin/tax-rates/{id}/rules](https://docs.medusajs.com/api/admin#tax-rates_posttaxratesidrules)
- [DELETE /admin/tax-rates/{id}/rules/{rule_id}](https://docs.medusajs.com/api/admin#tax-rates_deletetaxratesidrulesrule_id)
- [GET /admin/tax-regions](https://docs.medusajs.com/api/admin#tax-regions_gettaxregions)
- [POST /admin/tax-regions](https://docs.medusajs.com/api/admin#tax-regions_posttaxregions)
- [GET /admin/tax-regions/{id}](https://docs.medusajs.com/api/admin#tax-regions_gettaxregionsid)
- [POST /admin/tax-regions/{id}](https://docs.medusajs.com/api/admin#tax-regions_posttaxregionsid)
- [DELETE /admin/tax-regions/{id}](https://docs.medusajs.com/api/admin#tax-regions_deletetaxregionsid)
- [GET /admin/transaction-groups](https://docs.medusajs.com/api/admin#transaction-groups_gettransactiongroups)
- [GET /admin/translations](https://docs.medusajs.com/api/admin#translations_gettranslations)
- [POST /admin/translations/batch](https://docs.medusajs.com/api/admin#translations_posttranslationsbatch)
- [GET /admin/translations/entities](https://docs.medusajs.com/api/admin#translations_gettranslationsentities)
- [GET /admin/translations/settings](https://docs.medusajs.com/api/admin#translations_gettranslationssettings)
- [GET /admin/translations/statistics](https://docs.medusajs.com/api/admin#translations_gettranslationsstatistics)
- [POST /admin/uploads](https://docs.medusajs.com/api/admin#uploads_postuploads)
- [POST /admin/uploads/presigned-urls](https://docs.medusajs.com/api/admin#uploads_postuploadspresignedurls)
- [GET /admin/uploads/{id}](https://docs.medusajs.com/api/admin#uploads_getuploadsid)
- [DELETE /admin/uploads/{id}](https://docs.medusajs.com/api/admin#uploads_deleteuploadsid)
- [GET /admin/users](https://docs.medusajs.com/api/admin#users_getusers)
- [GET /admin/users/me](https://docs.medusajs.com/api/admin#users_getusersme)
- [GET /admin/users/{id}](https://docs.medusajs.com/api/admin#users_getusersid)
- [POST /admin/users/{id}](https://docs.medusajs.com/api/admin#users_postusersid)
- [DELETE /admin/users/{id}](https://docs.medusajs.com/api/admin#users_deleteusersid)
- [GET /admin/views/{entity}/columns](https://docs.medusajs.com/api/admin#views_getviewsentitycolumns)
- [GET /admin/views/{entity}/configurations](https://docs.medusajs.com/api/admin#views_getviewsentityconfigurations)
- [POST /admin/views/{entity}/configurations](https://docs.medusajs.com/api/admin#views_postviewsentityconfigurations)
- [GET /admin/views/{entity}/configurations/active](https://docs.medusajs.com/api/admin#views_getviewsentityconfigurationsactive)
- [POST /admin/views/{entity}/configurations/active](https://docs.medusajs.com/api/admin#views_postviewsentityconfigurationsactive)
- [GET /admin/views/{entity}/configurations/{id}](https://docs.medusajs.com/api/admin#views_getviewsentityconfigurationsid)
- [POST /admin/views/{entity}/configurations/{id}](https://docs.medusajs.com/api/admin#views_postviewsentityconfigurationsid)
- [DELETE /admin/views/{entity}/configurations/{id}](https://docs.medusajs.com/api/admin#views_deleteviewsentityconfigurationsid)
- [GET /admin/workflows-executions](https://docs.medusajs.com/api/admin#workflows-executions_getworkflowsexecutions)
- [GET /admin/workflows-executions/{id}](https://docs.medusajs.com/api/admin#workflows-executions_getworkflowsexecutionsid)
- [POST /admin/workflows-executions/{workflow_id}/run](https://docs.medusajs.com/api/admin#workflows-executions_postworkflowsexecutionsworkflow_idrun)
- [POST /admin/workflows-executions/{workflow_id}/steps/failure](https://docs.medusajs.com/api/admin#workflows-executions_postworkflowsexecutionsworkflow_idstepsfailure)
- [POST /admin/workflows-executions/{workflow_id}/steps/success](https://docs.medusajs.com/api/admin#workflows-executions_postworkflowsexecutionsworkflow_idstepssuccess)
- [GET /admin/workflows-executions/{workflow_id}/subscribe](https://docs.medusajs.com/api/admin#workflows-executions_getworkflowsexecutionsworkflow_idsubscribe)
- [GET /admin/workflows-executions/{workflow_id}/{transaction_id}](https://docs.medusajs.com/api/admin#workflows-executions_getworkflowsexecutionsworkflow_idtransaction_id)
- [GET /admin/workflows-executions/{workflow_id}/{transaction_id}/{step_id}/subscribe](https://docs.medusajs.com/api/admin#workflows-executions_getworkflowsexecutionsworkflow_idtransaction_idstep_idsubscribe)
- [POST /auth/session](https://docs.medusajs.com/api/admin#auth_postsession)
- [DELETE /auth/session](https://docs.medusajs.com/api/admin#auth_deletesession)
- [POST /auth/token/refresh](https://docs.medusajs.com/api/admin#auth_postadminauthtokenrefresh)
- [POST /auth/user/{auth_provider}](https://docs.medusajs.com/api/admin#auth_postactor_typeauth_provider)
- [POST /auth/user/{auth_provider}/callback](https://docs.medusajs.com/api/admin#auth_postactor_typeauth_providercallback)
- [POST /auth/user/{auth_provider}/register](https://docs.medusajs.com/api/admin#auth_postactor_typeauth_provider_register)
- [POST /auth/user/{auth_provider}/reset-password](https://docs.medusajs.com/api/admin#auth_postactor_typeauth_providerresetpassword)
- [POST /auth/user/{auth_provider}/update](https://docs.medusajs.com/api/admin#auth_postactor_typeauth_providerupdate)


## Store API Reference

- [POST /auth/customer/{auth_provider}](https://docs.medusajs.com/api/store#auth_postactor_typeauth_provider)
- [POST /auth/customer/{auth_provider}/callback](https://docs.medusajs.com/api/store#auth_postactor_typeauth_providercallback)
- [POST /auth/customer/{auth_provider}/register](https://docs.medusajs.com/api/store#auth_postactor_typeauth_provider_register)
- [POST /auth/customer/{auth_provider}/reset-password](https://docs.medusajs.com/api/store#auth_postactor_typeauth_providerresetpassword)
- [POST /auth/customer/{auth_provider}/update](https://docs.medusajs.com/api/store#auth_postactor_typeauth_providerupdate)
- [POST /auth/session](https://docs.medusajs.com/api/store#auth_postsession)
- [DELETE /auth/session](https://docs.medusajs.com/api/store#auth_deletesession)
- [POST /auth/token/refresh](https://docs.medusajs.com/api/store#auth_postadminauthtokenrefresh)
- [POST /store/carts](https://docs.medusajs.com/api/store#carts_postcarts)
- [GET /store/carts/{id}](https://docs.medusajs.com/api/store#carts_getcartsid)
- [POST /store/carts/{id}](https://docs.medusajs.com/api/store#carts_postcartsid)
- [POST /store/carts/{id}/complete](https://docs.medusajs.com/api/store#carts_postcartsidcomplete)
- [POST /store/carts/{id}/customer](https://docs.medusajs.com/api/store#carts_postcartsidcustomer)
- [POST /store/carts/{id}/gift-cards](https://docs.medusajs.com/api/store#carts_postcartsidgiftcards)
- [DELETE /store/carts/{id}/gift-cards](https://docs.medusajs.com/api/store#carts_deletecartsidgiftcards)
- [POST /store/carts/{id}/line-items](https://docs.medusajs.com/api/store#carts_postcartsidlineitems)
- [POST /store/carts/{id}/line-items/{line_id}](https://docs.medusajs.com/api/store#carts_postcartsidlineitemsline_id)
- [DELETE /store/carts/{id}/line-items/{line_id}](https://docs.medusajs.com/api/store#carts_deletecartsidlineitemsline_id)
- [POST /store/carts/{id}/promotions](https://docs.medusajs.com/api/store#carts_postcartsidpromotions)
- [DELETE /store/carts/{id}/promotions](https://docs.medusajs.com/api/store#carts_deletecartsidpromotions)
- [POST /store/carts/{id}/shipping-methods](https://docs.medusajs.com/api/store#carts_postcartsidshippingmethods)
- [POST /store/carts/{id}/store-credits](https://docs.medusajs.com/api/store#carts_postcartsidstorecredits)
- [POST /store/carts/{id}/taxes](https://docs.medusajs.com/api/store#carts_postcartsidtaxes)
- [GET /store/collections](https://docs.medusajs.com/api/store#collections_getcollections)
- [GET /store/collections/{id}](https://docs.medusajs.com/api/store#collections_getcollectionsid)
- [GET /store/currencies](https://docs.medusajs.com/api/store#currencies_getcurrencies)
- [GET /store/currencies/{code}](https://docs.medusajs.com/api/store#currencies_getcurrenciescode)
- [POST /store/customers](https://docs.medusajs.com/api/store#customers_postcustomers)
- [GET /store/customers/me](https://docs.medusajs.com/api/store#customers_getcustomersme)
- [POST /store/customers/me](https://docs.medusajs.com/api/store#customers_postcustomersme)
- [GET /store/customers/me/addresses](https://docs.medusajs.com/api/store#customers_getcustomersmeaddresses)
- [POST /store/customers/me/addresses](https://docs.medusajs.com/api/store#customers_postcustomersmeaddresses)
- [GET /store/customers/me/addresses/{address_id}](https://docs.medusajs.com/api/store#customers_getcustomersmeaddressesaddress_id)
- [POST /store/customers/me/addresses/{address_id}](https://docs.medusajs.com/api/store#customers_postcustomersmeaddressesaddress_id)
- [DELETE /store/customers/me/addresses/{address_id}](https://docs.medusajs.com/api/store#customers_deletecustomersmeaddressesaddress_id)
- [GET /store/gift-cards/{idOrCode}](https://docs.medusajs.com/api/store#gift-cards_getgiftcardsidorcode)
- [POST /store/gift-cards/{idOrCode}/redeem](https://docs.medusajs.com/api/store#gift-cards_postgiftcardsidorcoderedeem)
- [GET /store/locales](https://docs.medusajs.com/api/store#locales_getlocales)
- [GET /store/orders](https://docs.medusajs.com/api/store#orders_getorders)
- [GET /store/orders/{id}](https://docs.medusajs.com/api/store#orders_getordersid)
- [POST /store/orders/{id}/transfer/accept](https://docs.medusajs.com/api/store#orders_postordersidtransferaccept)
- [POST /store/orders/{id}/transfer/cancel](https://docs.medusajs.com/api/store#orders_postordersidtransfercancel)
- [POST /store/orders/{id}/transfer/decline](https://docs.medusajs.com/api/store#orders_postordersidtransferdecline)
- [POST /store/orders/{id}/transfer/request](https://docs.medusajs.com/api/store#orders_postordersidtransferrequest)
- [POST /store/payment-collections](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollections)
- [POST /store/payment-collections/{id}/payment-sessions](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions)
- [GET /store/payment-providers](https://docs.medusajs.com/api/store#payment-providers_getpaymentproviders)
- [GET /store/product-categories](https://docs.medusajs.com/api/store#product-categories_getproductcategories)
- [GET /store/product-categories/{id}](https://docs.medusajs.com/api/store#product-categories_getproductcategoriesid)
- [GET /store/product-tags](https://docs.medusajs.com/api/store#product-tags_getproducttags)
- [GET /store/product-tags/{id}](https://docs.medusajs.com/api/store#product-tags_getproducttagsid)
- [GET /store/product-types](https://docs.medusajs.com/api/store#product-types_getproducttypes)
- [GET /store/product-types/{id}](https://docs.medusajs.com/api/store#product-types_getproducttypesid)
- [GET /store/products](https://docs.medusajs.com/api/store#products_getproducts)
- [GET /store/products/{id}](https://docs.medusajs.com/api/store#products_getproductsid)
- [GET /store/regions](https://docs.medusajs.com/api/store#regions_getregions)
- [GET /store/regions/{id}](https://docs.medusajs.com/api/store#regions_getregionsid)
- [GET /store/return-reasons](https://docs.medusajs.com/api/store#return-reasons_getreturnreasons)
- [GET /store/return-reasons/{id}](https://docs.medusajs.com/api/store#return-reasons_getreturnreasonsid)
- [POST /store/returns](https://docs.medusajs.com/api/store#returns_postreturns)
- [GET /store/shipping-options](https://docs.medusajs.com/api/store#shipping-options_getshippingoptions)
- [POST /store/shipping-options/{id}/calculate](https://docs.medusajs.com/api/store#shipping-options_postshippingoptionsidcalculate)
- [GET /store/store-credit-accounts](https://docs.medusajs.com/api/store#store-credit-accounts_getstorecreditaccounts)
- [GET /store/store-credit-accounts/{id}](https://docs.medusajs.com/api/store#store-credit-accounts_getstorecreditaccountsid)


# Alert

A component for displaying important messages.

In this guide, you'll learn how to use the Alert component.

```tsx
import { Alert } from "@medusajs/ui"

export default function AlertDemo() {
  return <Alert>You are viewing Medusa docs.</Alert>
}

```

***

## Usage

```tsx
import { Alert } from "@medusajs/ui"
```

```tsx
<Alert>Here's a message</Alert>
```

***

## API Reference

### Alert Props

This component is based on the div element and supports all of its props

- variant: (union) The variant of the alert Default: "info"
- dismissible: (boolean) Whether the alert is dismissible Default: false

***

## Examples

### Success Alert

```tsx
import { Alert } from "@medusajs/ui"

export default function AlertSuccess() {
  return <Alert variant="success">Data updated successfully!</Alert>
}

```

### Warning Alert

```tsx
import { Alert } from "@medusajs/ui"

export default function AlertWarning() {
  return <Alert variant="warning">Be careful!</Alert>
}

```

### Error Alert

```tsx
import { Alert } from "@medusajs/ui"

export default function AlertError() {
  return <Alert variant="error">An error occured while updating data.</Alert>
}

```

### Dismissible Alert

```tsx
import { Alert } from "@medusajs/ui"

export default function AlertDismissable() {
  return <Alert dismissible={true}>You are viewing Medusa docs.</Alert>
}

```


# Avatar

A component for displaying user avatars with a fallback option.

In this guide, you'll learn how to use the Avatar component.

```tsx
import { Avatar } from "@medusajs/ui"

export default function AvatarDemo() {
  return (
    <Avatar
      src="https://avatars.githubusercontent.com/u/10656202?v=4"
      fallback="M"
    />
  )
}

```

## Usage

```tsx
import { Avatar } from "@medusajs/ui"
```

```tsx
<Avatar
  src="https://avatars.githubusercontent.com/u/10656202?v=4"
  fallback="M"
/>
```

***

## API Reference

### Avatar Props

This component is based on the \[Radix UI Avatar]\(https://www.radix-ui.com/primitives/docs/components/avatar) primitive.

- src: (string) The URL of the image used in the Avatar.
- fallback: (string) Text to show in the avatar if the URL provided in \`src\` can't be opened.
- variant: (union) The style of the avatar. Default: "rounded"
- size: (union) The size of the avatar's border radius. Default: "base"

***

## Examples

### Avatar Variants

```tsx
import { Avatar } from "@medusajs/ui"

export default function AvatarVariants() {
  return (
    <div className="flex gap-4">
      <Avatar
        src="https://avatars.githubusercontent.com/u/10656202?v=4"
        fallback="M"
        variant="rounded"
      />
      <Avatar
        src="https://avatars.githubusercontent.com/u/10656202?v=4"
        fallback="M"
        variant="squared"
      />
    </div>
  )
}

```

### Avatar Sizes

```tsx
import { Avatar } from "@medusajs/ui"

export default function AvatarSizes() {
  return (
    <div className="flex gap-4 items-center">
      <Avatar
        src="https://avatars.githubusercontent.com/u/10656202?v=4"
        fallback="M"
        size="2xsmall"
      />
      <Avatar
        src="https://avatars.githubusercontent.com/u/10656202?v=4"
        fallback="M"
        size="xsmall"
      />
      <Avatar
        src="https://avatars.githubusercontent.com/u/10656202?v=4"
        fallback="M"
        size="small"
      />
      <Avatar
        src="https://avatars.githubusercontent.com/u/10656202?v=4"
        fallback="M"
        size="base"
      />
      <Avatar
        src="https://avatars.githubusercontent.com/u/10656202?v=4"
        fallback="M"
        size="large"
      />
      <Avatar
        src="https://avatars.githubusercontent.com/u/10656202?v=4"
        fallback="M"
        size="xlarge"
      />
    </div>
  )
}

```

### Avatar Fallback Only

```tsx
import { Avatar } from "@medusajs/ui"

export default function AvatarFallback() {
  return <Avatar fallback="JD" />
}

```

### Avatar Custom Styling

```tsx
import { Avatar } from "@medusajs/ui"

export default function AvatarCustomStyle() {
  return (
    <Avatar
      src="https://avatars.githubusercontent.com/u/10656202?v=4"
      fallback="M"
      style={{
        boxShadow: "0 0 0 3px #fdba74, 0 1px 2px 0 rgba(0,0,0,0.05)",
        border: "none",
      }}
    />
  )
}

```

### Avatar Accessibility

You can add the `aria-label` prop to the Avatar component for better accessibility.

```tsx
import { Avatar } from "@medusajs/ui"

export default function AvatarAccessible() {
  return (
    <Avatar
      src="https://avatars.githubusercontent.com/u/10656202?v=4"
      fallback="M"
      aria-label="Medusa User"
    />
  )
}

```


# Badge

A component for displaying labels or indicators in a badge style.

In this guide, you'll learn how to use the Badge component.

```tsx
import { Badge } from "@medusajs/ui"

export default function BadgeDemo() {
  return <Badge>Badge</Badge>
}

```

## Usage

```tsx
import { Badge } from "@medusajs/ui"
```

```tsx
<Badge>Badge</Badge>
```

***

## API Reference

### Badge Props

This component is based on the \`div\` element and supports all of its props

- asChild: (boolean) Whether to remove the wrapper \`span\` element and use the
  passed child element instead. Default: false
- size: (union) The badge's size. Default: "base"
- rounded: (union) The style of the badge's border radius. Default: "base"
- color: (union) The badge's color. Default: "grey"

***

## Examples

### Badge Colors

```tsx
import { Badge } from "@medusajs/ui"

export default function BadgeAllColors() {
  return (
    <div className="flex gap-3">
      <Badge color="grey">Grey</Badge>
      <Badge color="red">Red</Badge>
      <Badge color="green">Green</Badge>
      <Badge color="blue">Blue</Badge>
      <Badge color="orange">Orange</Badge>
      <Badge color="purple">Purple</Badge>
    </div>
  )
}

```

### Badge Sizes

```tsx
import { Badge } from "@medusajs/ui"

export default function BadgeAllSizes() {
  return (
    <div className="flex gap-3 items-center">
      <Badge size="2xsmall">2xsmall</Badge>
      <Badge size="xsmall">xsmall</Badge>
      <Badge size="small">small</Badge>
      <Badge size="base">base</Badge>
      <Badge size="large">large</Badge>
    </div>
  )
}

```

### Badge Rounded Variants

```tsx
import { Badge } from "@medusajs/ui"

export default function BadgeAllRounded() {
  return (
    <div className="flex gap-3">
      <Badge rounded="base">Base Rounded</Badge>
      <Badge rounded="full">Full Rounded</Badge>
    </div>
  )
}

```


# Button

A component for rendering buttons using Medusa's design system.

In this guide, you'll learn how to use the Button component.

```tsx
import { Button } from "@medusajs/ui"

export default function ButtonDemo() {
  return <Button>Button</Button>
}

```

***

## Usage

```tsx
import { Button } from "@medusajs/ui"
```

```tsx
<Button>Button</Button>
```

***

## API Reference

### Button Props

This component is based on the \`button\` element and supports all of its props

- isLoading: (boolean) Whether to show a loading spinner. Default: false
- asChild: (boolean) Whether to remove the wrapper \`button\` element and use the
  passed child element instead. Default: false
- variant: (union) The button's style. Default: "primary"
- size: (union) The button's size. Default: "base"

***

## Examples

### Button Variants

```tsx
import { Button } from "@medusajs/ui"

export default function ButtonAllVariants() {
  return (
    <div className="flex gap-4">
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="transparent">Transparent</Button>
      <Button variant="danger">Danger</Button>
    </div>
  )
}

```

### Button Sizes

```tsx
import { Button } from "@medusajs/ui"

export default function ButtonAllSizes() {
  return (
    <div className="flex gap-4 items-center">
      <Button size="small">Small</Button>
      <Button size="base">Base</Button>
      <Button size="large">Large</Button>
      <Button size="xlarge">XLarge</Button>
    </div>
  )
}

```

### Button Loading State

```tsx
import { Button } from "@medusajs/ui"

export default function ButtonLoading() {
  return <Button isLoading={true}>Button</Button>
}

```

### Button with Icon

```tsx
import { PlusMini } from "@medusajs/icons"
import { Button } from "@medusajs/ui"

export default function ButtonWithIcon() {
  return (
    <Button>
      Button <PlusMini />
    </Button>
  )
}

```

### Button as Link

```tsx
import { Button } from "@medusajs/ui"

export default function ButtonAsLink() {
  return (
    <Button asChild>
      <a href="https://medusajs.com" target="_blank" rel="noopener noreferrer">
        Open Medusa Website
      </a>
    </Button>
  )
}

```


# Calendar

A component for displaying a calendar interface with date selection capability.

In this guide, you'll learn how to use the Calendar component.

```tsx
import { Calendar } from "@medusajs/ui"
import * as React from "react"

export default function CalendarDemo() {
  const [date, setDate] = React.useState<Date | null>()

  return <Calendar value={date} onChange={setDate} />
}

```

## Usage

```tsx
import { Calendar } from "@medusajs/ui"
```

```tsx
<Calendar />
```

***

## API Reference

### Calendar Props

Calendar component used to select a date.
Its props are based on \[React Aria Calendar]\(https://react-spectrum.adobe.com/react-aria/Calendar.html#calendar-1).

- value: (union) The currently selected date.
- defaultValue: (union) The date that is selected when the calendar first mounts (uncontrolled).
- onChange: (signature) A function that is triggered when the selected date changes.
- isDateUnavailable: (signature) A function that determines whether a date is unavailable for selection.
- minValue: (Date) The minimum date that can be selected.
- maxValue: (Date) The maximum date that can be selected.

***

## Examples

### Controlled

```tsx
import { Calendar } from "@medusajs/ui"
import { useState } from "react"

export default function CalendarControlled() {
  const [date, setDate] = useState<Date | null>(null)
  return (
    <div className="flex flex-col gap-2">
      <Calendar value={date} onChange={setDate} />
      <span className="txt-small text-ui-fg-muted">
        Selected: {date?.toDateString() ?? "None"}
      </span>
    </div>
  )
}

```

### Min/Max Dates

```tsx
import { Calendar } from "@medusajs/ui"

export default function CalendarMinMax() {
  const min = new Date()
  const max = new Date()
  max.setDate(max.getDate() + 10)
  return (
    <div className="flex flex-col gap-2">
      <span className="txt-small text-ui-fg-muted">
        Selectable dates: {min.toDateString()} to {max.toDateString()}
      </span>
      <Calendar minValue={min} maxValue={max} />
    </div>
  )
}

```

### Unavailable Dates

```tsx
import { Calendar } from "@medusajs/ui"

function isUnavailable(date: Date) {
  // Disable all Sundays
  return date.getDay() === 0
}

export default function CalendarUnavailable() {
  return (
    <div className="flex flex-col gap-2">
      <span className="txt-small text-ui-fg-muted">
        All Sundays are unavailable for selection.
      </span>
      <Calendar isDateUnavailable={isUnavailable} />
    </div>
  )
}

```


# Checkbox

A component for rendering checkbox inputs using Medusa's design system.

In this guide, you'll learn how to use the Checkbox component.

```tsx
import { Checkbox, Label } from "@medusajs/ui"

export default function CheckboxDemo() {
  return (
    <div className="flex items-center space-x-2">
      <Checkbox id="billing-shipping" />
      <Label htmlFor="billing-shipping">
        Billing address same as shipping address
      </Label>
    </div>
  )
}

```

## Usage

```tsx
import { Checkbox } from "@medusajs/ui"
```

```tsx
<Checkbox />
```

***

## API Reference

### Checkbox Props

This component is based on the \[Radix UI Checkbox]\(https://www.radix-ui.com/primitives/docs/components/checkbox) primitive.



***

## Examples

### Checkbox All States

```tsx
import { Checkbox, Label } from "@medusajs/ui"

export default function CheckboxAllStates() {
  return (
    <div className="flex flex-col gap-6">
      <div className="flex items-center gap-1">
        <Checkbox id="default" />
        <Label htmlFor="default">Default</Label>
      </div>
      <div className="flex items-center gap-1">
        <Checkbox id="checked" checked />
        <Label htmlFor="checked">Checked</Label>
      </div>
      <div className="flex items-center gap-1">
        <Checkbox id="disabled" disabled />
        <Label htmlFor="disabled">Disabled</Label>
      </div>
      <div className="flex items-center gap-1">
        <Checkbox id="indeterminate" checked="indeterminate" />
        <Label htmlFor="indeterminate">Indeterminate</Label>
      </div>
    </div>
  )
}

```

### Controlled Checkbox

```tsx
import { Checkbox, CheckboxCheckedState, Label } from "@medusajs/ui"
import { useState } from "react"

export default function CheckboxControlled() {
  const [checked, setChecked] = useState<CheckboxCheckedState>(false)

  const handleToggle = () => {
    switch (checked) {
      case "indeterminate":
        setChecked(true)
        return
      case true:
        setChecked(false)
        return
      default:
        setChecked("indeterminate")
    }
  }

  return (
    <div className="flex flex-col gap-6 items-center">
      <span className="txt-small text-center w-3/4">
        The following checkbox will move from unchecked, to indeterminate, and
        finally checked each time you click it
      </span>
      <div className="flex items-center gap-2">
        <Checkbox
          id="controlled-checkbox"
          checked={checked}
          onCheckedChange={handleToggle}
        />
        <Label htmlFor="controlled-checkbox">
          Controlled Checkbox: (
          {checked === "indeterminate"
            ? "Indeterminate"
            : checked
              ? "Checked"
              : "Unchecked"}
          )
        </Label>
      </div>
    </div>
  )
}

```


# Code Block

A component for displaying code snippets with syntax highlighting and copy functionality.

In this guide, you'll learn how to use the Code Block component.

```tsx
import { CodeBlock, Label } from "@medusajs/ui"

const snippets = [
  {
    label: "cURL",
    language: "bash",
    code: `curl 'http://localhost:9000/store/products/PRODUCT_ID'\n -H 'x-publishable-key: YOUR_API_KEY'`,
    hideLineNumbers: true,
  },
  {
    label: "Medusa JS SDK",
    language: "jsx",
    code: `const { product } = await medusa.store.products.retrieve("prod_123")\nconsole.log(product.id)`,
  },
]

export default function CodeBlockDemo() {
  return (
    <div className="w-full">
      <CodeBlock snippets={snippets}>
        <CodeBlock.Header>
          <CodeBlock.Header.Meta>
            <Label weight={"plus"}>/product-detail.js</Label>
          </CodeBlock.Header.Meta>
        </CodeBlock.Header>
        <CodeBlock.Body />
      </CodeBlock>
    </div>
  )
}

```

## Usage

```tsx
import { CodeBlock } from "@medusajs/ui"
```

```tsx
<CodeBlock 
  snippets={[
    { 
      language: "tsx", 
      label: "Label", 
      code: "import { useProduct } from \"medusa-react\"",
    },
  ]}
>
  <CodeBlock.Header />
  <CodeBlock.Body />
</CodeBlock>
```

***

## API Reference

### CodeBlock Props

This component is based on the \`div\` element and supports all of its props

- snippets: (Array) The code snippets.

### CodeBlock.Header Props

This component is based on the \`div\` element and supports all of its props

- hideLabels: (boolean) Whether to hide the code snippets' labels. Default: false

### CodeBlock.Header.Meta Props

This component is based on the \`div\` element and supports all of its props



### CodeBlock.Body Props

This component is based on the \`div\` element and supports all of its props



***

## Usage Outside Medusa Admin

If you're using the `CodeBlock` component in a project other than the Medusa Admin, make sure to include the `TooltipProvider` somewhere up in your component tree, as the `CodeBlock.Header` component uses a [Tooltip](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/ui/app/components/tooltip#usage-outside-medusa-admin/index.html.md):

```tsx
<TooltipProvider>
  <CodeBlock 
    snippets={[
      { 
        language: "tsx", 
        label: "Label", 
        code: "import { useProduct } from \"medusa-react\"",
      },
    ]}
  >
    <CodeBlock.Header />
    <CodeBlock.Body />
  </CodeBlock>
</TooltipProvider>
```

***

## Examples

### Single Snippet

If you want to only show a code sample for one language or API, you can choose to hide the snippet labels:

```tsx
import { CodeBlock, Label } from "@medusajs/ui"

const snippets = [
  {
    label: "Medusa JS SDK",
    language: "jsx",
    code: `console.log("Hello, World!")`,
  },
]

export default function CodeBlockSingle() {
  return (
    <div className="w-full">
      <CodeBlock snippets={snippets}>
        <CodeBlock.Header hideLabels={true}>
          <CodeBlock.Header.Meta>
            <Label weight={"plus"}>/product-detail.js</Label>
          </CodeBlock.Header.Meta>
        </CodeBlock.Header>
        <CodeBlock.Body />
      </CodeBlock>
    </div>
  )
}

```

### No Header

You can also omit the header entirely:

```tsx
import { CodeBlock } from "@medusajs/ui"

const snippets = [
  {
    label: "Medusa JS SDK",
    language: "jsx",
    code: `console.log("Hello, World!")`,
  },
]

export default function CodeBlockNoHeader() {
  return (
    <div className="w-full">
      <CodeBlock snippets={snippets}>
        <CodeBlock.Body />
      </CodeBlock>
    </div>
  )
}

```

### No Line Numbers

```tsx
import { CodeBlock, Label } from "@medusajs/ui"

const snippets = [
  {
    label: "Medusa JS SDK",
    language: "jsx",
    code: `console.log("Hello, ")\n\nconsole.log("World!")`,
    hideLineNumbers: true,
  },
]

export default function CodeBlockNoLines() {
  return (
    <div className="w-full">
      <CodeBlock snippets={snippets}>
        <CodeBlock.Header>
          <CodeBlock.Header.Meta>
            <Label weight={"plus"}>/product-detail.js</Label>
          </CodeBlock.Header.Meta>
        </CodeBlock.Header>
        <CodeBlock.Body />
      </CodeBlock>
    </div>
  )
}

```

### No Copy Button

```tsx
import { CodeBlock, Label } from "@medusajs/ui"

const snippets = [
  {
    label: "Medusa JS SDK",
    language: "jsx",
    code: `console.log("Hello, World!")`,
    hideCopy: true,
  },
]

export default function CodeBlockNoCopy() {
  return (
    <div className="w-full">
      <CodeBlock snippets={snippets}>
        <CodeBlock.Header>
          <CodeBlock.Header.Meta>
            <Label weight={"plus"}>/product-detail.js</Label>
          </CodeBlock.Header.Meta>
        </CodeBlock.Header>
        <CodeBlock.Body />
      </CodeBlock>
    </div>
  )
}

```


# Command Bar

A component that displays a command bar with a list of commands to perform on a bulk selection of items.

In this guide, you'll learn how to use the Command Bar component.

```tsx
import { Checkbox, CommandBar, Label, Text } from "@medusajs/ui"
import * as React from "react"

export default function CommandBarDemo() {
  const [selected, setSelected] = React.useState<boolean>(false)

  return (
    <div className="flex justify-center gap-y-2 flex-col">
      <div className="flex items-center gap-x-2">
        <Checkbox
          checked={selected}
          onCheckedChange={(checked) =>
            setSelected(checked === true ? true : false)
          }
        />
        <Label>Item One</Label>
      </div>
      <div><Text size="small" className="text-ui-fg-muted">Check the box to view the command bar</Text></div>
      <CommandBar open={selected}>
        <CommandBar.Bar>
          <CommandBar.Value>1 selected</CommandBar.Value>
          <CommandBar.Seperator />
          <CommandBar.Command
            action={() => {
              alert("Delete")
            }}
            label="Delete"
            shortcut="d"
          />
          <CommandBar.Seperator />
          <CommandBar.Command
            action={() => {
              alert("Edit")
            }}
            label="Edit"
            shortcut="e"
          />
        </CommandBar.Bar>
      </CommandBar>
    </div>
  )
}

```

## Usage

```tsx
import { CommandBar } from "@medusajs/ui"
```

```tsx
<CommandBar open={open}>
  <CommandBar.Bar>
    <CommandBar.Value>{count} selected</CommandBar.Value>
    <CommandBar.Seperator />
    <CommandBar.Command
      action={onDelete}
      label="Delete"
      shortcut="d"
    />
    <CommandBar.Seperator />
    <CommandBar.Command
      action={onEdit}
      label="Edit"
      shortcut="e"
    />
  </CommandBar.Bar>
</CommandBar>
```

***

## API Reference

### CommandBar Props

The root component of the command bar. This component manages the state of the command bar.

- open: (boolean) Whether to open (show) the command bar. Default: false
- onOpenChange: (signature) Specify a function to handle the change of \`open\`'s value.
- defaultOpen: (boolean) Whether the command bar is open by default. Default: false
- disableAutoFocus: (boolean) Whether to disable focusing automatically on the command bar when it's opened. Default: true

### CommandBar.Bar Props

The bar component of the command bar. This component is used to display the commands.



### CommandBar.Value Props

The value component of the command bar. This component is used to display a value,
such as the number of selected items which the commands will act on.



### CommandBar.Seperator Props

The seperator component of the command bar. This component is used to display a seperator between commands.



### CommandBar.Command Props

The command component of the command bar. This component is used to display a command, as well as registering the keyboad shortcut.

- action: (signature) The function to execute when the command is triggered.
- label: (string) The command's label.
- shortcut: (string) The command's shortcut


# Command

A component that renders an unhighlighted code block, useful for one-liners or API routes.

In this guide, you'll learn how to use the Command component.

```tsx
import { Badge, Command } from "@medusajs/ui"

export default function CommandDemo() {
  return (
    <div className="w-full">
      <Command>
        <Badge color="green">Get</Badge>
        <code>localhost:9000/store/products</code>
        <Command.Copy
          content="localhost:9000/store/products"
          className="ml-auto"
        />
      </Command>
    </div>
  )
}

```

## Usage

```tsx
import { Command } from "@medusajs/ui"
```

```tsx
<Command>
  <code>yarn add @medusajs/ui</code>
</Command>
```

***

## API Reference

### Command Props

This component is based on the div element and supports all of its props



***

## Usage Outside Medusa Admin

If you're using the `Command` component in a project other than the Medusa Admin, make sure to include the `TooltipProvider` somewhere up in your component tree, as the `Command.Copy` component uses a [Tooltip](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/ui/app/components/tooltip#usage-outside-medusa-admin/index.html.md):

```tsx
<TooltipProvider>
  <Command>
    <code>yarn add @medusajs/ui</code>
    <Command.Copy
      content="yarn add @medusajs/ui"
      className="ml-auto"
    />
  </Command>
</TooltipProvider>
```


# Container

A component that wraps content in a card-like container. The container is useful to create sections in the Medusa Admin dashboard.

In this guide, you'll learn how to use the Container component.

```tsx
import { Container } from "@medusajs/ui"

export default function ContainerDemo() {
  return <Container>Content</Container>
}

```

## Usage

```tsx
import { Container } from "@medusajs/ui"
```

```tsx
<Container>Container</Container>
```

***

## API Reference

### Container Props

This component is based on the \`div\` element and supports all of its props



***

## Examples

### In a Layout

```tsx
import { Container, Heading } from "@medusajs/ui"

export default function ContainerLayout() {
  return (
    <div className="flex h-full w-full">
      <div className="border-ui-border-base w-full max-w-[216px] border-r p-4">
        <Heading level="h3">Menubar</Heading>
      </div>
      <div className="flex w-full flex-col gap-y-2 px-8 pb-8 pt-6">
        <Container>
          <Heading>Section 1</Heading>
        </Container>
        <Container>
          <Heading>Section 2</Heading>
        </Container>
        <Container>
          <Heading>Section 3</Heading>
        </Container>
      </div>
    </div>
  )
}

```


# Copy

A component that wraps content in a button with copy functionality. It is useful for quickly copying text to the clipboard, such as code snippets or configuration commands.

In this guide, you'll learn how to use the Copy component.

```tsx
import { Copy } from "@medusajs/ui"

export default function CopyDemo() {
  return <Copy content="yarn add @medusajs/ui" />
}

```

## Usage

```tsx
import { Copy } from "@medusajs/ui"
```

```tsx
<Copy content="yarn add @medusajs/ui" />
```

***

## API Reference

### Copy Props

This component is based on the \`button\` element and supports all of its props

- content: (string) The content to copy.
- variant: (union) The variant of the copy button. Default: "default"
- asChild: (boolean) Whether to remove the wrapper \`button\` element and use the
  passed child element instead. Default: false

***

## Usage Outside Medusa Admin

If you're using the `Copy` component in a project other than the Medusa Admin, make sure to include the `TooltipProvider` somewhere up in your component tree, as the `Copy` component uses a [Tooltip](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/ui/app/components/tooltip#usage-outside-medusa-admin/index.html.md):

```tsx
<TooltipProvider>
  <Copy content="yarn add @medusajs/ui" />
</TooltipProvider>
```

***

## Examples

### Copy with Custom Display

```tsx
import { Code, Copy } from "@medusajs/ui"

export default function CopyDemo() {
  return (
    <Copy content="yarn add @medusajs/ui">
      <Code>yarn add @medusajs/ui</Code>
    </Copy>
  )
}

```

### Copy Display As Child

Using the `asChild` prop, you can render the `<Copy/>` as its child. This is useful if you want to render a custom button, to prevent rendering a button inside a button.

```tsx
import { PlusMini } from "@medusajs/icons"
import { Copy, IconButton, Text } from "@medusajs/ui"

export default function CopyAsChild() {
  return (
    <div className="flex items-center gap-x-2">
      <Text>Copy command</Text>
      <Copy content="yarn add @medusajs/ui" asChild>
        <IconButton>
          <PlusMini />
        </IconButton>
      </Copy>
    </div>
  )
}

```


# Currency Input

A component for rendering form inputs for money amounts, showing the currency in the input.

In this guide, you'll learn how to use the Currency Input component.

```tsx
import { CurrencyInput } from "@medusajs/ui"

export default function CurrencyInputDemo() {
  return (
    <div className="max-w-[250px]">
      <CurrencyInput symbol="$" code="usd" />
    </div>
  )
}

```

## Usage

```tsx
import { CurrencyInput } from "@medusajs/ui"
```

```tsx
<CurrencyInput symbol="$" code="usd" />
```

***

## API Reference

### CurrencyInput Props

This component is based on the input element and supports all of its props

- symbol: (string) The symbol to show in the input.
- code: (string) The currency code to show in the input.
- size: (union) The input's size. Default: "base"
- disabled: (boolean) Whether the input is disabled.


  &#x20;Default: false
- onInvalid: (undefined) A function that is triggered when the input is invalid.

***

## Examples

### Controlled Currency Input

```tsx
import { useState } from "react"
import { CurrencyInput } from "@medusajs/ui"

export default function CurrencyInputControlled() {
  const [value, setValue] = useState<string | undefined>("")
  const formatValue = (val: string | undefined) => {
    if (!val) {
      return ""
    }
    return new Intl.NumberFormat("en-US", {
      style: "currency",
      currency: "USD",
    }).format(parseFloat(val))
  }
  return (
    <div className="max-w-[250px]">
      <CurrencyInput
        symbol="$"
        code="usd"
        value={value}
        onValueChange={setValue}
        aria-label="Amount"
      />
      <div className="mt-2 text-xs text-ui-fg-muted">
        Value: {formatValue(value)}
      </div>
    </div>
  )
}

```

### Disabled Currency Input

```tsx
import { CurrencyInput } from "@medusajs/ui"

export default function CurrencyInputDisabled() {
  return (
    <div className="max-w-[250px]">
      <CurrencyInput
        symbol="€"
        code="eur"
        disabled
        value={"100"}
        aria-label="Amount"
      />
    </div>
  )
}

```

### Currency Input with Error State

```tsx
import { useState } from "react"
import { CurrencyInput } from "@medusajs/ui"

export default function CurrencyInputError() {
  const [value, setValue] = useState<string | undefined>("0")
  const [touched, setTouched] = useState(false)
  const isError = touched && (!value || parseFloat(value) <= 0)
  return (
    <div className="max-w-[250px]">
      <CurrencyInput
        symbol="$"
        code="usd"
        value={value}
        onValueChange={(val) => setValue(val)}
        aria-label="Amount"
        aria-invalid={isError}
        onBlur={() => setTouched(true)}
        min={0.01}
      />
      {isError && (
        <div className="mt-2 text-xs text-ui-fg-error">
          Amount must be greater than 0
        </div>
      )}
    </div>
  )
}

```

### Currency Input Sizes

#### Base

```tsx
import { CurrencyInput } from "@medusajs/ui"

export default function CurrencyInputBase() {
  return (
    <div className="max-w-[250px]">
      <CurrencyInput size="base" symbol="$" code="usd" />
    </div>
  )
}

```

#### Small

```tsx
import { CurrencyInput } from "@medusajs/ui"

export default function CurrencyInputSmall() {
  return (
    <div className="max-w-[250px]">
      <CurrencyInput size="small" symbol="$" code="usd" />
    </div>
  )
}

```


# Data Table

A component to display a table of data with advanced functionalities like pagination, filtering, and more.

In this guide, you'll learn how to use the DataTable component.

The `DataTable` component is useful if you're displaying large data with functionalities like pagination, filtering, sorting, and searching. It's also the recommended table component to use when creating customizations in the Medusa Admin.

This component is available after Medusa UI v4.0.4 (or Medusa v2.4.0). It is built on top of the [Table](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/ui/app/components/table/index.html.md) component. If you want a table with more control over its styling and functionality, use that component instead.

```tsx
import { createDataTableColumnHelper, useDataTable, DataTable, Heading } from "@medusajs/ui"

const products = [
  {
    id: "1",
    title: "Shirt",
    price: 10
  },
  {
    id: "2",
    title: "Pants",
    price: 20
  }
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    enableSorting: true,
  }),
  columnHelper.accessor("price", {
    header: "Price",
    enableSorting: true,
  }),
]

export default function ProductTable () {
  const table = useDataTable({
    columns,
    data: products,
    getRowId: (product) => product.id,
    rowCount: products.length,
    isLoading: false,
  })
  
  return (
    <DataTable instance={table}>
	    <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
	      <Heading>Products</Heading>
      </DataTable.Toolbar>
      <DataTable.Table />
    </DataTable>
  )
}
```

## Usage

You import the `DataTable` component from `@medusajs/ui`.

```tsx
import { 
  DataTable,
} from "@medusajs/ui"
```

### Data Table Columns Preparation

Before using the `DataTable` component, you need to prepare its columns using the `createDataTableColumnHelper` utility:

```tsx
import {
  // ...
  createDataTableColumnHelper,
} from "@medusajs/ui"

const data = [
  {
    id: "1",
    title: "Shirt",
    price: 10,
  },
  // other data...
]

const columnHelper = createDataTableColumnHelper<typeof data[0]>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    enableSorting: true,
  }),
  columnHelper.accessor("price", {
    header: "Price",
  }),
]
```

The `createDataTableColumnHelper` utility is a function that returns a helper used to generate column configurations for the `DataTable` component.

For each column in the table, use the `accessor` method of the column helper to specify configurations for a specific column. The `accessor` method accepts the column's key in the table's data as the first parameter, and an object with the following properties as the second parameter:

- `header`: The table header text for the column.
- `enableSorting`: (optional) A boolean that indicates whether data in the table can be sorted by this column. More on sorting in [this section](#configure-sorting-in-datatable).

### Create Data Table Instance

The `DataTable` component expects a table instance created using the `useDataTable` hook. Import that hook from `@medusajs/ui`:

```tsx
import {
  // ...
  useDataTable,
} from "@medusajs/ui"
```

Then, inside the component that will render `DataTable`, create a table instance using the `useDataTable` hook:

```tsx
export default function ProductTable() {
  const table = useDataTable({
    columns,
    data,
    getRowId: (product) => product.id,
    rowCount: data.length,
    isLoading: false,
  })
}
```

The `useDataTable` hook accepts an object with the following properties:

- `columns`: An array of column configurations generated using the `createDataTableColumnHelper` utility.
- `data`: The data to be displayed in the table.
- `getRowId`: A function that returns the unique identifier of a row. The identifier must be a string.
- `rowCount`: The total number of rows in the table. If you're fetching data from the Medusa application with pagination or filters, this will be the total count, not the count of the data returned in the current page.
- `isLoading`: A boolean that indicates whether the table is loading data. This is useful when loading data from the Medusa application for the first time or in between pages.

### Render DataTable

Finally, render the `DataTable` component with the table instance created using the `useDataTable` hook:

```tsx
export default function ProductTable() {
  // ...
  return (
    <DataTable instance={table}>
      <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
        <Heading>Products</Heading>
      </DataTable.Toolbar>
      <DataTable.Table />
    </DataTable>
  )
}
```

In the `DataTable` component, you pass the following child components:

1. `DataTable.Toolbar`: The toolbar component shown at the top of the table. You can also add buttons for custom actions.
2. `DataTable.Table`: The table component that renders the data.

Refer to the examples later on this page to learn how to add pagination, filtering, and other functionalities using the `DataTable` component.

***

## API Reference

### DataTable Props

This component creates a data table with filters, pagination, sorting, and more.
It's built on top of the \`Table\` component while expanding its functionality.
The \`DataTable\` is useful to create tables similar to those in the Medusa Admin dashboard.

- instance: (UseDataTableReturn) The instance returned by the \`useDataTable\` hook.
- children: (ReactReactNode) The children of the component.
- className: (string) Additional classes to pass to the wrapper \`div\` of the component.

### DataTable.Table Props

This component renders the table in a data table, supporting advanced features.

- emptyState: (signature) The empty state to display when the table is empty.

### DataTable.Pagination Props

This component adds a pagination component and functionality to the data table.

- translations: (ReactComponentProps\["translations"]) The translations for strings in the pagination component.

### DataTable.FilterMenu Props

This component adds a filter menu to the data table, allowing users
to filter the table's data.

- tooltip: (string) The tooltip to show when hovering over the filter menu.
- onAddFilter: (signature) Callback when a filter is added

### DataTable.Search Props

This component adds a search input to the data table, allowing users
to search through the table's data.

- autoFocus: (boolean) If true, the search input will be focused on mount.
- className: (string) Additional classes to pass to the search input.
- placeholder: (string) The placeholder text to show in the search input.

### DataTable.CommandBar Props

This component adds a command bar to the data table, which is used
to show commands that can be executed on the selected rows.

- selectedLabel: (union) The label to show when items are selected. If a function is passed,&#x20;
  it will be called with the count of selected items.

### DataTable.SortingMenu Props

This component adds a sorting menu to the data table, allowing users
to sort the table's data.

- tooltip: (string) The tooltip to show when hovering over the sorting menu.

***

## Example with Data Fetching

Refer to [this Admin Components guide](https://docs.medusajs.com/resources/admin-components/components/data-table/index.html.md) for an example on using the `DataTable` component with data fetching from the Medusa application.

***

## Handle Row Click

```tsx
import {
  createDataTableColumnHelper,
  useDataTable,
  DataTable,
  Heading,
  usePrompt,
} from "@medusajs/ui"

const products = [
  {
    id: "1",
    title: "Shirt",
    price: 10,
  },
  {
    id: "2",
    title: "Pants",
    price: 20,
  },
]

const columnHelper = createDataTableColumnHelper<(typeof products)[0]>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    enableSorting: true,
  }),
  columnHelper.accessor("price", {
    header: "Price",
    enableSorting: true,
  }),
]

export default function ProductTable() {
  const prompt = usePrompt()

  const table = useDataTable({
    columns,
    data: products,
    getRowId: (product) => product.id,
    rowCount: products.length,
    isLoading: false,
    onRowClick: async (event, row) => {
      await prompt({
        title: "Row Clicked",
        description: `You clicked row #${row.id}`,
      })
    },
  })

  return (
    <DataTable instance={table}>
      <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
        <Heading>Products</Heading>
      </DataTable.Toolbar>
      <DataTable.Table />
    </DataTable>
  )
}

```

In many cases, you want to perform an action when a row is clicked. Most commonly, you may want to open the details page of the row when it's clicked.

For bulk actions, such as deleting multiple rows, use the [Command Bar](#perform-bulk-actions-on-datatable-rows) instead.

The `useDataTable` hook accepts an `onRowClick` property that you can use to handle row clicks:

```tsx
const navigate = useNavigate()

const table = useDataTable({
  // ...
  onRowClick(event, row) {
    navigate(`/author/${row.id}`)
  },
})
```

The value of `onRowClick` is a function that accepts two parameters:

- `event`: An instance of the [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) object.
- `row`: The data of the row that was clicked.

In the above example, you use a `navigate` function, retrieved through the `useNavigate` hook from `react-router-dom`, to navigate to the details page of the row that was clicked.

***

## Configure Cell Rendering

```tsx
import { createDataTableColumnHelper, useDataTable, DataTable, Heading, Badge } from "@medusajs/ui"

const products = [
  {
    id: "1",
    title: "Shirt",
    price: 10,
    is_active: true
  },
  {
    id: "2",
    title: "Pants",
    price: 20,
    is_active: false
  }
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    enableSorting: true,
  }),
  columnHelper.accessor("price", {
    header: "Price",
    enableSorting: true,
  }),
  columnHelper.accessor("is_active", {
    header: "Status",
    cell: ({ getValue }) => {
      const isActive = getValue()
      return (
        <Badge color={isActive ? "green" : "grey"} size="xsmall">
          {isActive ? "Active" : "Inactive"}
        </Badge>
      )
    }
  })
]

export default function ProductTable () {
  const table = useDataTable({
    columns,
    data: products,
    getRowId: (product) => product.id,
    rowCount: products.length,
    isLoading: false,
  })
  
  return (
    <DataTable instance={table}>
	    <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
	      <Heading>Products</Heading>
      </DataTable.Toolbar>
      <DataTable.Table />
    </DataTable>
  )
}
```

The `accessor` method of the `createDataTableColumnHelper` utility accepts a `cell` property that you can use to customize the rendering of the cell content.

For example:

```tsx
const products = [
  {
    id: "1",
    title: "Shirt",
    price: 10,
    is_active: true,
  },
  {
    id: "2",
    title: "Pants",
    price: 20,
    is_active: true,
  },
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  columnHelper.accessor("is_active", {
    header: "Status",
    cell: ({ getValue }) => {
      const isActive = getValue()
      return (
        <Badge color={isActive ? "green" : "grey"}>
          {isActive ? "Active" : "Inactive"}
        </Badge>
      )
    },
  }),
  // ...
]
```

The `cell` property's value is a function that returns a string or a React node to be rendered in the cell. The function receives as a parameter an object having a `getValue` property to get the raw value of the cell.

***

## Configure Search in DataTable

```tsx
import { createDataTableColumnHelper, useDataTable, DataTable, Heading } from "@medusajs/ui"
import { useMemo, useState } from "react"

const products = [
  {
    id: "1",
    title: "Shirt",
    price: 10
  },
  {
    id: "2",
    title: "Pants",
    price: 20
  }
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    enableSorting: true,
  }),
  columnHelper.accessor("price", {
    header: "Price",
    enableSorting: true,
  }),
]

export default function ProductTable () {
	const [search, setSearch] = useState<string>("")

  const shownProducts = useMemo(() => {
    return products.filter((product) => product.title.toLowerCase().includes(search.toLowerCase()))
  }, [search]) 
  
  const table = useDataTable({
    columns,
    data: shownProducts,
    getRowId: (product) => product.id,
    rowCount: products.length,
    isLoading: false,
    // Pass the state and onSearchChange to the table instance.
    search: {
	    state: search,
	    onSearchChange: setSearch
    }
  })
  
  return (
    <DataTable instance={table}>
	    <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
	      <Heading>Products</Heading>
	      {/* This component renders the search bar */}
	      <DataTable.Search placeholder="Search..." />
      </DataTable.Toolbar>
      <DataTable.Table />
    </DataTable>
  )
}
```

The object passed to the `useDataTable` hook accepts a `search` property that you can use to enable and configure the search functionality in the `DataTable` component:

```tsx
// `useState` imported from `React`
const [search, setSearch] = useState("")

const table = useDataTable({
  // ...
  search: {
    state: search,
    onSearchChange: setSearch,
  },
})
```

`search` accepts the following properties:

- `state`: The search query string. This must be a React state variable, as its value will be used for the table's search input.
- `onSearchChange`: A function that updates the search query string. Typically, this would be the setter function of the state variable, but you can also perform custom actions if necessary.

Next, you must implement the search filtering. For example, if you're retrieving data from the Medusa application, you pass the search query to the API to filter the data.

For example, when using a simple array as in the example above, this is how you filter the data by the search query:

```tsx
const [search, setSearch] = useState<string>("")

const shownProducts = useMemo(() => {
  return products.filter((product) => product.title.toLowerCase().includes(search.toLowerCase()))
}, [search]) 

const table = useDataTable({
  columns,
  data: shownProducts,
  getRowId: (product) => product.id,
  rowCount: products.length,
  isLoading: false,
  // Pass the state and onSearchChange to the table instance.
  search: {
    state: search,
    onSearchChange: setSearch,
  },
})
```

Then, render the `DataTable.Search` component as part of the `DataTable`'s children:

```tsx
return (
  <DataTable instance={table}>
    <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
      <Heading>Products</Heading>
      {/* This component renders the search bar */}
      <DataTable.Search placeholder="Search..." />
    </DataTable.Toolbar>
    <DataTable.Table />
  </DataTable>
)
```

This will show a search input at the top of the table, in the data table's toolbar.

***

## Configure Pagination in DataTable

```tsx
import { DataTable, Heading, createDataTableColumnHelper, useDataTable, type DataTablePaginationState } from "@medusajs/ui"
import { useMemo, useState } from "react"

const products = [
  {
    id: "1",
    title: "Shirt",
    price: 10
  },
  {
    id: "2",
    title: "Pants",
    price: 20
  },
  {
    id: "3",
    title: "Hat",
    price: 15
  },
  {
    id: "4",
    title: "Socks",
    price: 5
  },
  {
    id: "5",
    title: "Shoes",
    price: 50
  },
  {
    id: "6",
    title: "Jacket",
    price: 100
  },
  {
    id: "7",
    title: "Scarf",
    price: 25
  },
  {
    id: "8",
    title: "Gloves",
    price: 12
  },
  {
    id: "9",
    title: "Belt",
    price: 18
  },
  {
    id: "10",
    title: "Sunglasses",
    price: 30
  },
  {
    id: "11",
    title: "Watch",
    price: 200
  },
  {
    id: "12",
    title: "Tie",
    price: 20
  },
  {
    id: "13",
    title: "Sweater",
    price: 40
  },
  {
    id: "14",
    title: "Jeans",
    price: 60
  },
  {
    id: "15",
    title: "Shorts",
    price: 25
  },
  {
    id: "16",
    title: "Blouse",
    price: 35
  },
  {
    id: "17",
    title: "Dress",
    price: 80
  }
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    enableSorting: true,
  }),
  columnHelper.accessor("price", {
    header: "Price",
    enableSorting: true,
  }),
]

const PAGE_SIZE = 10;

export default function ProductTable () {
  const [pagination, setPagination] = useState<DataTablePaginationState>({
    pageSize: PAGE_SIZE,
    pageIndex: 0,
  })

  const shownProducts = useMemo(() => {
    return products.slice(
      pagination.pageIndex * pagination.pageSize,
      (pagination.pageIndex + 1) * pagination.pageSize
    )
  }, [pagination])

  const table = useDataTable({
    data: shownProducts,
    columns,
    rowCount: products.length,
    getRowId: (product) => product.id,
    pagination: {
      // Pass the pagination state and updater to the table instance
      state: pagination,
      onPaginationChange: setPagination,
    },
    isLoading: false,
  });

  return (
      <DataTable instance={table}>
        <DataTable.Toolbar>
          <Heading>Products</Heading>
        </DataTable.Toolbar>
				<DataTable.Table />
        {/** This component will render the pagination controls **/}
        <DataTable.Pagination />
      </DataTable>
  );
};
```

The object passed to the `useDataTable` hook accepts a `pagination` property that you can use to enable and configure the pagination functionality in the `DataTable` component.

First, import the `DataTablePaginationState` type from `@medusajs/ui`:

```tsx
import {
  // ...
  DataTablePaginationState,
} from "@medusajs/ui"
```

Then, create a state variable to manage the pagination:

```tsx
const [pagination, setPagination] = useState<DataTablePaginationState>({
  pageSize: 15,
  pageIndex: 0,
})
```

The pagination state variable of type `DataTablePaginationState` is an object with the following properties:

- `pageSize`: The number of rows to display per page.
- `pageIndex`: The current page index. It's zero-based, meaning the first page would be `0`.

Next, pass the pagination object to the `useDataTable` hook:

```tsx
const table = useDataTable({
  // ...
  pagination: {
    state: pagination,
    onPaginationChange: setPagination,
  },
})
```

`pagination` accepts the following properties:

- `state`: The pagination state object. This must be a React state variable of type `DataTablePaginationState`.
- `onPaginationChange`: A function that updates the pagination state object. Typically, this would be the setter function of the state variable, but you can also perform custom actions if necessary.

You must also implement the pagination logic, such as fetching data from the Medusa application with the pagination parameters.

For example, when using a simple array as in the example above, this is how you paginate the data:

```tsx
const [pagination, setPagination] = useState<DataTablePaginationState>({
  pageSize: PAGE_SIZE,
  pageIndex: 0,
})

const shownProducts = useMemo(() => {
  return products.slice(
    pagination.pageIndex * pagination.pageSize,
    (pagination.pageIndex + 1) * pagination.pageSize
  )
}, [pagination])

const table = useDataTable({
  data: shownProducts,
  columns,
  rowCount: products.length,
  getRowId: (product) => product.id,
  pagination: {
    // Pass the pagination state and updater to the table instance
    state: pagination,
    onPaginationChange: setPagination,
  },
  isLoading: false,
})
```

Finally, render the `DataTable.Pagination` component as part of the `DataTable`'s children:

```tsx
return (
  <DataTable instance={table}>
    <DataTable.Toolbar>
      <Heading>Products</Heading>
    </DataTable.Toolbar>
    <DataTable.Table />
    {/** This component will render the pagination controls **/}
    <DataTable.Pagination />
  </DataTable>
)
```

This will show the pagination controls at the end of the table.

***

## Configure Filters in DataTable

```tsx
import { DataTable, DataTableFilteringState, Heading, createDataTableColumnHelper, createDataTableFilterHelper, useDataTable } from "@medusajs/ui"
import { useMemo, useState } from "react"

const products = [
  {
    id: "1",
    title: "Shirt",
    price: 10
  },
  {
    id: "2",
    title: "Pants",
    price: 20
  }
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    enableSorting: true,
  }),
  columnHelper.accessor("price", {
    header: "Price",
    enableSorting: true,
  }),
]

const filterHelper = createDataTableFilterHelper<typeof products[0]>()

const filters = [
  filterHelper.accessor("title", {
    type: "select",
    label: "Title",
    options: products.map((product) => ({
      label: product.title,
      value: product.title.toLowerCase()
    }))
  }),
]

export default function ProductTable () {
	const [filtering, setFiltering] = useState<DataTableFilteringState>({})

  const shownProducts = useMemo(() => {
    return products.filter((product) => {
      return Object.entries(filtering).every(([key, value]) => {
        if (!value) {
          return true
        }
        if (typeof value === "string") {
          // @ts-ignore
          return product[key].toString().toLowerCase().includes(value.toString().toLowerCase())
        }
        if (Array.isArray(value)) {
          // @ts-ignore
          return value.includes(product[key].toLowerCase())
        }
        if (typeof value === "object") {
          // @ts-ignore
          const date = new Date(product[key])
          let matching = false
          if ("$gte" in value && value.$gte) {
            matching = date >= new Date(value.$gte as number)
          }
          if ("$lte" in value && value.$lte) {
            matching = date <= new Date(value.$lte as number)
          }
          if ("$lt" in value && value.$lt) {
            matching = date < new Date(value.$lt as number)
          }
          if ("$gt" in value && value.$gt) {
            matching = date > new Date(value.$gt as number)
          }
          return matching
        }
      })
    })
  }, [filtering])

  const table = useDataTable({
    data: shownProducts,
    columns,
    getRowId: (product) => product.id,
    rowCount: products.length,
    isLoading: false,
    filtering: {
      state: filtering,
      onFilteringChange: setFiltering,
    },
    filters
  });

  return (
    <DataTable instance={table}>
      <DataTable.Toolbar className="flex justify-between items-center">
        <Heading>Products</Heading>
        {/** This component will render a menu that allows the user to choose which filters to apply to the table data. **/}
        <DataTable.FilterMenu tooltip="Filter" />
      </DataTable.Toolbar>
	    <DataTable.Table />
    </DataTable>
  );
};
```

The object passed to the `useDataTable` hook accepts a `filters` property that you can use to enable and configure the filtering functionality in the `DataTable` component.

First, add the following imports from the `@medusajs/ui` package:

```tsx
import {
  // ...
  createDataTableFilterHelper,
  DataTableFilteringState,
} from "@medusajs/ui"
```

The `createDataTableFilterHelper` utility is a function that returns a helper function to generate filter configurations for the `DataTable` component. The `DataTableFilteringState` type is an object that represents the filtering state of the table.

Then, create the filters using the `createDataTableFilterHelper` utility:

Create the filters outside the component rendering the `DataTable` component.

```tsx
const filterHelper = createDataTableFilterHelper<typeof products[0]>()

const filters = [
  filterHelper.accessor("title", {
    type: "select",
    label: "Title",
    options: products.map((product) => ({
      label: product.title,
      value: product.title.toLowerCase(),
    })),
  }),
]
```

The filter helper returned by `createDataTableFilterHelper` has an `accessor` method that accepts the column's key in the data as the first parameter, and an object with the following properties as the second parameter:

- `type`: The type of filter. It can be either:
  - `select`: A select dropdown filter.
  - `radio`: A radio button filter.
  - `date`: A date filter.
- `label`: The label text for the filter.
- `options`: If the filter type is `select` or `radio`, an array of dropdown options. Each option has a `label` and `value` property.

Refer to [this section](#filtering-date-values) to learn how to use date filters.

Next, in the component rendering the `DataTable` component, create a state variable to manage the filtering, and pass the filters to the `useDataTable` hook:

```tsx
const [filtering, setFiltering] = useState<DataTableFilteringState>({})

const table = useDataTable({
  // ...
  filters,
  filtering: {
    state: filtering,
    onFilteringChange: setFiltering,
  },
})
```

You create a `filtering` state variable of type `DataTableFilteringState` to manage the filtering state. You can also set initial filters as explained in [this section](#initial-filter-values).

The `useDataTable` hook accepts the following properties for filtering:

- `filters`: An array of filter configurations generated using the `createDataTableFilterHelper` utility.
- `filtering`: An object with the following properties:
  - `state`: The filtering state object. This must be a React state variable of type `DataTableFilteringState`.
  - `onFilteringChange`: A function that updates the filtering state object. Typically, this would be the setter function of the state variable, but you can also perform custom actions if necessary.

You must also implement the logic of filtering the data based on the filter conditions, such as sending the filter conditions to the Medusa application when fetching data.

For example, when using a simple array as in the example above, this is how you filter the data based on the filter conditions:

```tsx
const [filtering, setFiltering] = useState<DataTableFilteringState>({})

const shownProducts = useMemo(() => {
  return products.filter((product) => {
    return Object.entries(filtering).every(([key, value]) => {
      if (!value) {
        return true
      }
      if (typeof value === "string") {
        // @ts-ignore
        return product[key].toString().toLowerCase().includes(value.toString().toLowerCase())
      }
      if (Array.isArray(value)) {
        // @ts-ignore
        return value.includes(product[key].toLowerCase())
      }
      if (typeof value === "object") {
        // @ts-ignore
        const date = new Date(product[key])
        let matching = false
        if ("$gte" in value && value.$gte) {
          matching = date >= new Date(value.$gte as number)
        }
        if ("$lte" in value && value.$lte) {
          matching = date <= new Date(value.$lte as number)
        }
        if ("$lt" in value && value.$lt) {
          matching = date < new Date(value.$lt as number)
        }
        if ("$gt" in value && value.$gt) {
          matching = date > new Date(value.$gt as number)
        }
        return matching
      }
    })
  })
}, [filtering])

const table = useDataTable({
  data: shownProducts,
  columns,
  getRowId: (product) => product.id,
  rowCount: products.length,
  isLoading: false,
  filtering: {
    state: filtering,
    onFilteringChange: setFiltering,
  },
  filters,
})
```

When filters are selected, the `filtering` state object will contain the filter conditions, where the key is the column key and the value can be:

- `undefined` if the user is still selecting the value.
- A string if the filter type is `radio`, as the user can choose only one value.
- An array of strings if the filter type is `select`, as the user can choose multiple values.
- An object with the filter conditions if the filter type is `date`. The filter conditions for dates are explained more in [this section](#filtering-date-values).

Finally, render the `DataTable.FilterMenu` component as part of the `DataTable`'s children:

```tsx
return (
  <DataTable instance={table}>
    <DataTable.Toolbar className="flex justify-between items-center">
      <Heading>Products</Heading>
      {/** This component will render a menu that allows the user to choose which filters to apply to the table data. **/}
      <DataTable.FilterMenu tooltip="Filter" />
    </DataTable.Toolbar>
    <DataTable.Table />
  </DataTable>
)
```

This will show a filter menu at the top of the table, in the data table's toolbar.

### Filtering Date Values in DataTable

```tsx
import { DataTable, DataTableFilteringState, Heading, createDataTableColumnHelper, createDataTableFilterHelper, useDataTable } from "@medusajs/ui"
import { useMemo, useState } from "react"

const products = [
  {
    id: "1",
    title: "Shirt",
    price: 10,
    created_at: new Date()
  },
  {
    id: "2",
    title: "Pants",
    price: 20,
    created_at: new Date("2026-01-01")
  }
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    enableSorting: true,
  }),
  columnHelper.accessor("price", {
    header: "Price",
    enableSorting: true,
  }),
  columnHelper.accessor("created_at", {
    header: "Created At",
    cell: ({ getValue }) => {
      return getValue().toLocaleString()
    }
  }),
]

const filterHelper = createDataTableFilterHelper<typeof products[0]>()

const filters = [
  filterHelper.accessor("created_at", {
    type: "date",
    label: "Created At",
    format: "date",
    formatDateValue: (date) => date.toLocaleString(),
    rangeOptionStartLabel: "From",
    rangeOptionEndLabel: "To",
    rangeOptionLabel: "Between",
    options: [
      {
        label: "Today",
        value: {
          $gte: new Date(new Date().setHours(0, 0, 0, 0)).toString(),
          $lte: new Date(new Date().setHours(23, 59, 59, 999)).toString()
        }
      },
      {
        label: "Yesterday",
        value: {
          $gte: new Date(new Date().setHours(0, 0, 0, 0) - 24 * 60 * 60 * 1000).toString(),
          $lte: new Date(new Date().setHours(0, 0, 0, 0)).toString()
        }
      },
      {
        label: "Last Week",
        value: {
          $gte: new Date(new Date().setHours(0, 0, 0, 0) - 7 * 24 * 60 * 60 * 1000).toString(),
          $lte: new Date(new Date().setHours(0, 0, 0, 0)).toString()
        },
      },
    ]
  }),
]

export default function ProductTable () {
	const [filtering, setFiltering] = useState<DataTableFilteringState>({})

  const shownProducts = useMemo(() => {
    return products.filter((product) => {
      return Object.entries(filtering).every(([key, value]) => {
        if (!value) {
          return true
        }
        if (typeof value === "string") {
          // @ts-ignore
          return product[key].toString().toLowerCase().includes(value.toString().toLowerCase())
        }
        if (Array.isArray(value)) {
          // @ts-ignore
          return value.includes(product[key].toLowerCase())
        }
        if (typeof value === "object") {
          // @ts-ignore
          const date = new Date(product[key])
          let matching = false
          if ("$gte" in value && value.$gte) {
            matching = date >= new Date(value.$gte as number)
          }
          if ("$lte" in value && value.$lte) {
            matching = date <= new Date(value.$lte as number)
          }
          if ("$lt" in value && value.$lt) {
            matching = date < new Date(value.$lt as number)
          }
          if ("$gt" in value && value.$gt) {
            matching = date > new Date(value.$gt as number)
          }
          return matching
        }
      })
    })
  }, [filtering])

  const table = useDataTable({
    data: shownProducts,
    columns,
    getRowId: (product) => product.id,
    rowCount: products.length,
    isLoading: false,
    filtering: {
      state: filtering,
      onFilteringChange: setFiltering,
    },
    filters
  });

  return (
    <DataTable instance={table}>
      <DataTable.Toolbar className="flex justify-between items-center">
        <Heading>Products</Heading>
        {/** This component will render a menu that allows the user to choose which filters to apply to the table data. **/}
        <DataTable.FilterMenu tooltip="Filter" />
      </DataTable.Toolbar>
	    <DataTable.Table />
    </DataTable>
  );
};
```

Consider your data has a `created_at` field that contains date values. To filter the data based on date values, you can add a `date` filter using the filter helper:

```tsx
const filters = [
  // ...
  filterHelper.accessor("created_at", {
    type: "date",
    label: "Created At",
    format: "date",
    formatDateValue: (date) => date.toLocaleString(),
    rangeOptionStartLabel: "From",
    rangeOptionEndLabel: "To",
    rangeOptionLabel: "Between",
    options: [
      {
        label: "Today",
        value: {
          $gte: new Date(new Date().setHours(0, 0, 0, 0)).toString(),
          $lte: new Date(new Date().setHours(23, 59, 59, 999)).toString(),
        },
      },
      {
        label: "Yesterday",
        value: {
          $gte: new Date(new Date().setHours(0, 0, 0, 0) - 24 * 60 * 60 * 1000).toString(),
          $lte: new Date(new Date().setHours(0, 0, 0, 0)).toString(),
        },
      },
      {
        label: "Last Week",
        value: {
          $gte: new Date(new Date().setHours(0, 0, 0, 0) - 7 * 24 * 60 * 60 * 1000).toString(),
          $lte: new Date(new Date().setHours(0, 0, 0, 0)).toString(),
        },
      },
    ],
  }),
]
```

When the filter type is `date`, the filter configuration object passed as a second parameter to the `accessor` method accepts the following properties:

- `format`: The format of the date value. It can be either `date` to filter by dates, or `datetime` to filter by dates and times.
- `formatDateValue`: A function that formats the date value when displaying it in the filter options.
- `rangeOptionStartLabel`: (optional) The label for the start date input in the range filter.
- `rangeOptionEndLabel`: (optional) The label for the end date input in the range filter.
- `rangeOptionLabel`: (optional) The label for the range filter option.
- `options`: By default, the filter will allow the user to filter between two dates. You can also set this property to an array of filter options to quickly choose from. Each option has a `label` and `value` property. The `value` property is an object that represents the filter condition. In this example, the filter condition is an object with a `$gte` property that specifies the date that the data should be greater than or equal to. Allowed properties are:
  - `$gt`: Greater than.
  - `$lt`: Less than.
  - `$lte`: Less than or equal to.
  - `$gte`: Greater than or equal to.

When the user selects a date filter option, the `filtering` state object will contain the filter conditions, where the key is the column key and the value is an object with the filter conditions. You must handle the filter logic as explained earlier.

For example, when using a simple array as in the example above, this is how you filter the data based on the date filter conditions:

```tsx
const shownProducts = useMemo(() => {
  return products.filter((product) => {
    return Object.entries(filtering).every(([key, value]) => {
      if (!value) {
        return true
      }
      // other types checks...
      if (typeof value === "object") {
        // @ts-ignore
        const date = new Date(product[key])
        let matching = false
        if ("$gte" in value && value.$gte) {
          matching = date >= new Date(value.$gte as number)
        }
        if ("$lte" in value && value.$lte) {
          matching = date <= new Date(value.$lte as number)
        }
        if ("$lt" in value && value.$lt) {
          matching = date < new Date(value.$lt as number)
        }
        if ("$gt" in value && value.$gt) {
          matching = date > new Date(value.$gt as number)
        }
        return matching
      }
    })
  })
}, [filtering])
```

### Initial Filter Values in DataTable

```tsx
import { DataTable, DataTableFilteringState, Heading, createDataTableColumnHelper, createDataTableFilterHelper, useDataTable } from "@medusajs/ui"
import { useMemo, useState } from "react"

const products = [
  {
    id: "1",
    title: "Shirt",
    price: 10
  },
  {
    id: "2",
    title: "Pants",
    price: 20
  }
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    enableSorting: true,
  }),
  columnHelper.accessor("price", {
    header: "Price",
    enableSorting: true,
  }),
]

const filterHelper = createDataTableFilterHelper<typeof products[0]>()

const filters = [
  filterHelper.accessor("title", {
    type: "select",
    label: "Title",
    options: products.map((product) => ({
      label: product.title,
      value: product.title.toLowerCase()
    }))
  }),
]

export default function ProductTable () {
	const [filtering, setFiltering] = useState<DataTableFilteringState>({
    title: ["shirt"]
  })

  const shownProducts = useMemo(() => {
    return products.filter((product) => {
      return Object.entries(filtering).every(([key, value]) => {
        if (!value) {
          return true
        }
        if (typeof value === "string") {
          // @ts-ignore
          return product[key].toString().toLowerCase().includes(value.toString().toLowerCase())
        }
        if (Array.isArray(value)) {
          // @ts-ignore
          return value.includes(product[key].toLowerCase())
        }
        if (typeof value === "object") {
          // @ts-ignore
          const date = new Date(product[key])
          let matching = false
          if ("$gte" in value && value.$gte) {
            matching = date >= new Date(value.$gte as number)
          }
          if ("$lte" in value && value.$lte) {
            matching = date <= new Date(value.$lte as number)
          }
          if ("$lt" in value && value.$lt) {
            matching = date < new Date(value.$lt as number)
          }
          if ("$gt" in value && value.$gt) {
            matching = date > new Date(value.$gt as number)
          }
          return matching
        }
      })
    })
  }, [filtering])

  const table = useDataTable({
    data: shownProducts,
    columns,
    getRowId: (product) => product.id,
    rowCount: products.length,
    isLoading: false,
    filtering: {
      state: filtering,
      onFilteringChange: setFiltering,
    },
    filters
  });

  return (
    <DataTable instance={table}>
      <DataTable.Toolbar className="flex justify-between items-center">
        <Heading>Products</Heading>
        {/** This component will render a menu that allows the user to choose which filters to apply to the table data. **/}
        <DataTable.FilterMenu tooltip="Filter" />
      </DataTable.Toolbar>
	    <DataTable.Table />
    </DataTable>
  );
};
```

If you want to set initial filter values, you can set the initial state of the `filtering` state variable:

```tsx
const [filtering, setFiltering] = useState<DataTableFilteringState>({
  title: ["shirt"],
})
```

The user can still change the filter values, but the initial values will be applied when the table is first rendered.

***

## Configure Sorting in DataTable

```tsx
import { DataTable, DataTableSortingState, Heading, createDataTableColumnHelper, useDataTable } from "@medusajs/ui"
import { useMemo, useState } from "react"

const products = [
  {
    id: "1",
    title: "Shirt",
    price: 10
  },
  {
    id: "2",
    title: "Pants",
    price: 20
  }
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    // Enables sorting for the column.
    enableSorting: true,
    // If omitted, the header will be used instead if it's a string, 
    // otherwise the accessor key (id) will be used.
    sortLabel: "Title",
    // If omitted the default value will be "A-Z"
    sortAscLabel: "A-Z",
    // If omitted the default value will be "Z-A"
    sortDescLabel: "Z-A",
  }),
  columnHelper.accessor("price", {
    header: "Price",
  }),
]

export default function ProductTable () {
  const [sorting, setSorting] = useState<DataTableSortingState | null>(null);

  const shownProducts = useMemo(() => {
    if (!sorting) {
      return products
    }
    return products.slice().sort((a, b) => {
      // @ts-ignore
      const aVal = a[sorting.id]
      // @ts-ignore
      const bVal = b[sorting.id]
      if (aVal < bVal) {
        return sorting.desc ? 1 : -1
      }
      if (aVal > bVal) {
        return sorting.desc ? -1 : 1
      }
      return 0
    })
  }, [sorting])

  const table = useDataTable({
    data: shownProducts,
    columns,
    getRowId: (product) => product.id,
    rowCount: products.length,
    sorting: {
      // Pass the pagination state and updater to the table instance
      state: sorting,
      onSortingChange: setSorting,
    },
    isLoading: false,
  })

  return (
    <DataTable instance={table}>
      <DataTable.Toolbar className="flex justify-between items-center">
        <Heading>Products</Heading>
        {/** This component will render a menu that allows the user to choose which column to sort by and in what direction. **/}
        <DataTable.SortingMenu tooltip="Sort" />
      </DataTable.Toolbar>
	    <DataTable.Table />
    </DataTable>
  );
};
```

The object passed to the `useDataTable` hook accepts a `sorting` property that you can use to enable and configure the sorting functionality in the `DataTable` component.

First, in the `columns` array created by the columns helper, specify for the sortable columns the following properties:

```tsx
const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    // Enables sorting for the column.
    enableSorting: true,
    // If omitted, the header will be used instead if it's a string, 
    // otherwise the accessor key (id) will be used.
    sortLabel: "Title",
    // If omitted the default value will be "A-Z"
    sortAscLabel: "A-Z",
    // If omitted the default value will be "Z-A"
    sortDescLabel: "Z-A",
  }),
]
```

The `accessor` method of the helper function accepts the following properties for sorting:

- `enableSorting`: A boolean that indicates whether data in the table can be sorted by this column.
- `sortLabel`: The label text for the sort button in the column header. If omitted, the `header` will be used instead if it's a string, otherwise the accessor key (id) will be used.
- `sortAscLabel`: The label text for the ascending sort button. If omitted, the default value will be `A-Z`.
- `sortDescLabel`: The label text for the descending sort button. If omitted, the default value will be `Z-A`.

Next, in the component rendering the `DataTable` component, create a state variable to manage the sorting, and pass the sorting object to the `useDataTable` hook:

```tsx
import {
  // ...
  DataTableSortingState,
} from "@medusajs/ui"

export default function ProductTable() {
  const [sorting, setSorting] = useState<DataTableSortingState | null>(null)

  const table = useDataTable({
    // ...
    sorting: {
      state: sorting,
      onSortingChange: setSorting,
    },
  })

  // ...
}
```

You create a state variable of type `DataTableSortingState` to manage the sorting state. You can also set initial sorting values as explained in [this section](#initial-sort-values).

The `sorting` object passed to the `useDataTable` hook accepts the following properties:

- `state`: The sorting state object. This must be a React state variable of type `DataTableSortingState`.
- `onSortingChange`: A function that updates the sorting state object. Typically, this would be the setter function of the state variable, but you can also perform custom actions if necessary.

You must also implement the sorting logic, such as sending the sorting conditions to the Medusa application when fetching data.

For example, when using a simple array as in the example above, this is how you sort the data based on the sorting conditions:

```tsx
const [sorting, setSorting] = useState<DataTableSortingState | null>(null)

const shownProducts = useMemo(() => {
  if (!sorting) {
    return products
  }
  return products.slice().sort((a, b) => {
    // @ts-ignore
    const aVal = a[sorting.id]
    // @ts-ignore
    const bVal = b[sorting.id]
    if (aVal < bVal) {
      return sorting.desc ? 1 : -1
    }
    if (aVal > bVal) {
      return sorting.desc ? -1 : 1
    }
    return 0
  })
}, [sorting])

const table = useDataTable({
  data: shownProducts,
  columns,
  getRowId: (product) => product.id,
  rowCount: products.length,
  sorting: {
    // Pass the pagination state and updater to the table instance
    state: sorting,
    onSortingChange: setSorting,
  },
  isLoading: false,
})
```

The `sorting` state object has the following properties:

- `id`: The column key to sort by.
- `desc`: A boolean that indicates whether to sort in descending order.

Finally, render the `DataTable.SortingMenu` component as part of the `DataTable`'s children:

```tsx
return (
  <DataTable instance={table}>
    <DataTable.Toolbar className="flex justify-between items-center">
      <Heading>Products</Heading>
      {/** This component will render a menu that allows the user to choose which column to sort by and in what direction. **/}
      <DataTable.SortingMenu tooltip="Sort" />
    </DataTable.Toolbar>
    <DataTable.Table />
  </DataTable>
)
```

This will show a sorting menu at the top of the table, in the data table's toolbar.

### Initial Sort Values in DataTable

```tsx
import { DataTable, DataTableSortingState, Heading, createDataTableColumnHelper, useDataTable } from "@medusajs/ui"
import { useMemo, useState } from "react"

const products = [
  {
    id: "1",
    title: "Shirt",
    price: 10
  },
  {
    id: "2",
    title: "Pants",
    price: 20
  }
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    // Enables sorting for the column.
    enableSorting: true,
    // If omitted, the header will be used instead if it's a string, 
    // otherwise the accessor key (id) will be used.
    sortLabel: "Title",
    // If omitted the default value will be "A-Z"
    sortAscLabel: "A-Z",
    // If omitted the default value will be "Z-A"
    sortDescLabel: "Z-A",
  }),
  columnHelper.accessor("price", {
    header: "Price",
  }),
]

export default function ProductTable () {
  const [sorting, setSorting] = useState<DataTableSortingState | null>({
    id: "title",
    desc: false,
  })

  const shownProducts = useMemo(() => {
    if (!sorting) {
      return products
    }
    return products.slice().sort((a, b) => {
      // @ts-ignore
      const aVal = a[sorting.id]
      // @ts-ignore
      const bVal = b[sorting.id]
      if (aVal < bVal) {
        return sorting.desc ? 1 : -1
      }
      if (aVal > bVal) {
        return sorting.desc ? -1 : 1
      }
      return 0
    })
  }, [sorting])

  const table = useDataTable({
    data: shownProducts,
    columns,
    getRowId: (product) => product.id,
    rowCount: products.length,
    sorting: {
      // Pass the pagination state and updater to the table instance
      state: sorting,
      onSortingChange: setSorting,
    },
    isLoading: false,
  })

  return (
    <DataTable instance={table}>
      <DataTable.Toolbar className="flex justify-between items-center">
        <Heading>Products</Heading>
        {/** This component will render a menu that allows the user to choose which column to sort by and in what direction. **/}
        <DataTable.SortingMenu tooltip="Sort" />
      </DataTable.Toolbar>
	    <DataTable.Table />
    </DataTable>
  );
};
```

If you want to set initial sort values, you can set the initial state of the `sorting` state variable:

```tsx
const [sorting, setSorting] = useState<DataTableSortingState | null>({
  id: "title",
  desc: false,
})
```

The user can still change the sort values, but the initial values will be applied when the table is first rendered.

***

## Perform Bulk Actions on DataTable Rows

```tsx
import { DataTable, DataTableRowSelectionState, Heading, createDataTableColumnHelper, createDataTableCommandHelper, useDataTable } from "@medusajs/ui"
import { useState } from "react"

let products = [
  {
    id: "1",
    title: "Shirt",
    price: 10,
  },
  {
    id: "2",
    title: "Pants",
    price: 20,
  }
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  // Commands requires a select column.
  columnHelper.select(),
  columnHelper.accessor("title", {
    header: "Title",
    enableSorting: true,
  }),
  columnHelper.accessor("price", {
    header: "Price",
    enableSorting: true,
  }),
]

const commandHelper = createDataTableCommandHelper()

const useCommands = () => {
  return [
    commandHelper.command({
      label: "Delete",
      shortcut: "D",
      action: async (selection) => {
        const productsToDeleteIds = Object.keys(selection)

        alert(`You deleted product(s) with IDs: ${productsToDeleteIds.join()}`)
      }
    })
  ]
}

export default function ProductTable () {
	const [rowSelection, setRowSelection] = useState<DataTableRowSelectionState>({})

  const commands = useCommands()

  const instance = useDataTable({
    data: products,
    columns,
    getRowId: (product) => product.id,
    rowCount: products.length,
    isLoading: false,
    commands,
    rowSelection: {
      state: rowSelection,
      onRowSelectionChange: setRowSelection,
    },
  });

  return (
    <DataTable instance={instance}>
      <DataTable.Toolbar className="flex justify-between items-center">
        <Heading>Products</Heading>
      </DataTable.Toolbar>
	    <DataTable.Table />
      {/** This component will the command bar when the user has selected at least one row. **/}
	    <DataTable.CommandBar selectedLabel={(count) => `${count} selected`} />
    </DataTable>
  );
};
```

The object passed to the `useDataTable` hook accepts a `commands` object property that you can use to add custom actions to the `DataTable` component.

First, add the following imports from `@medusajs/ui`:

```tsx
import {
  // ...
  createDataTableCommandHelper,
  DataTableRowSelectionState,
} from "@medusajs/ui"
```

The `createDataTableCommandHelper` utility is a function that returns a helper function to generate command configurations for the `DataTable` component. The `DataTableRowSelectionState` type is an object that represents the row selection state of the table.

Then, in the `columns` array created by the columns helper, add a `select` column:

```tsx
const columns = [
  // Commands requires a select column.
  columnHelper.select(),
  // ...
]
```

The `select` method of the helper function adds a select column to the table. This column will render checkboxes in each row to allow the user to select rows.

Next, create the commands using the `createDataTableCommandHelper` utility:

Create the commands outside the component rendering the `DataTable` component.

```tsx
const commandHelper = createDataTableCommandHelper()

const useCommands = () => {
  return [
    commandHelper.command({
      label: "Delete",
      shortcut: "D",
      action: async (selection) => {
        const productsToDeleteIds = Object.keys(selection)

        // TODO remove products from the server
      },
    }),
  ]
}
```

The `createDataTableCommandHelper` utility is a function that returns a helper function to generate command configurations for the `DataTable` component.

You create a function that returns an array of command configurations. This is useful if the command's action requires initializing other functions or hooks.

The `command` method of the helper function accepts the following properties:

- `label`: The label text for the command.
- `shortcut`: The keyboard shortcut for the command. This shortcut only works when rows are selected in the table.
- `action`: A function that performs the action when the command is executed. The function receives the selected rows as an object, where the key is the row's `id` field and the value is a boolean indicating that the row is selected. You can send a request to the server within this function to perform the action.

Then, in the component rendering the `DataTable` component, create a state variable to manage the selected rows, and pass the commands to the `useDataTable` hook:

```tsx
const [rowSelection, setRowSelection] = useState<DataTableRowSelectionState>({})

const commands = useCommands()

const instance = useDataTable({
  data: products,
  columns,
  getRowId: (product) => product.id,
  rowCount: products.length,
  isLoading: false,
  commands,
  rowSelection: {
    state: rowSelection,
    onRowSelectionChange: setRowSelection,
  },
})
```

You create a state variable of type `DataTableRowSelectionState` to manage the selected rows. You also retrieve the commands by calling the `useCommand` function.

The `useDataTable` hook accepts the following properties for commands:

- `commands`: An array of command configurations generated using the `createDataTableCommandHelper` utility.
- `rowSelection`: An object that enables selecting rows in the table. It accepts the following properties:
  - `state`: The row selection state object. This must be a React state variable of type `DataTableRowSelectionState`.
  - `onRowSelectionChange`: A function that updates the row selection state object. Typically, this would be the setter function of the state variable, but you can also perform custom actions if necessary.

Finally, render the `DataTable.CommandBar` component as part of the `DataTable`'s children:

```tsx
return (
  <DataTable instance={instance}>
    <DataTable.Toolbar className="flex justify-between items-center">
      <Heading>Products</Heading>
    </DataTable.Toolbar>
    <DataTable.Table />
    {/** This component will the command bar when the user has selected at least one row. **/}
    <DataTable.CommandBar selectedLabel={(count) => `${count} selected`} />
  </DataTable>
)
```

This will show a command bar when the user has selected at least one row in the table.


# Date Picker

A component for rendering date picker inputs with range and presets.

In this guide, you'll learn how to use the Date Picker component.

```tsx
import { DatePicker } from "@medusajs/ui"

export default function DatePickerDemo() {
  return (
    <div className="w-[250px]">
      <DatePicker />
    </div>
  )
}

```

## Usage

```tsx
import { DatePicker } from "@medusajs/ui"
```

```tsx
<DatePicker />
```

***

## API Reference

### DatePicker Props

- aria-describedby: (string) Identifies the element (or elements) that describes the object.
- aria-details: (string) Identifies the element (or elements) that provide a detailed, extended description for the object.
- aria-label: (string) Defines a string value that labels the current element.
- aria-labelledby: (string) Identifies the element (or elements) that labels the current element.
- autoComplete: (string) Describes the type of autocomplete functionality the input should provide if any. See \[MDN]\(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefautocomplete).
- autoFocus: (boolean) Whether the element should receive focus on render.
- defaultOpen: (boolean) Whether the overlay is open by default (uncontrolled).
- description: (ReactNode) A description for the field. Provides a hint such as specific requirements for what to choose.
- errorMessage: (union) An error message for the field.
- firstDayOfWeek: (union) The day that starts the week.
- form: (string) The \`\<form>\` element to associate the input with.
  The value of this attribute must be the id of a \`\<form>\` in the same document.
  See \[MDN]\(https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#form).
- hideTimeZone: (boolean) Whether to hide the time zone abbreviation.
- hourCycle: (union) Whether to display the time in 12 or 24 hour format. By default, this is determined by the user's locale.
- id: (string) The element's unique identifier. See \[MDN]\(https://developer.mozilla.org/en-US/docs/Web/HTML/Global\_attributes/id).
- isDisabled: (boolean) Whether the input is disabled.
- isInvalid: (boolean) Whether the input value is invalid.
- isOpen: (boolean) Whether the overlay is open by default (controlled).
- isReadOnly: (boolean) Whether the input can be selected but not changed by the user.
- isRequired: (boolean) Whether user input is required on the input before form submission.
- label: (ReactNode) The content to display as the label.
- name: (string) The name of the input element, used when submitting an HTML form. See \[MDN]\(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname).
- onBlur: (signature) Handler that is called when the element loses focus.
- onFocus: (signature) Handler that is called when the element receives focus.
- onFocusChange: (signature) Handler that is called when the element's focus status changes.
- onKeyDown: (signature) Handler that is called when a key is pressed.
- onKeyUp: (signature) Handler that is called when a key is released.
- onOpenChange: (signature) Handler that is called when the overlay's open state changes.
- pageBehavior: (PageBehavior) Controls the behavior of paging. Pagination either works by advancing the visible page by visibleDuration (default) or one unit of visibleDuration.
- placeholderValue: (union) A placeholder date that influences the format of the placeholder shown when no value is selected. Defaults to today's date at midnight.
- shouldForceLeadingZeros: (boolean) Whether to always show leading zeros in the month, day, and hour fields.
  By default, this is determined by the user's locale.
- validate: (signature) A function that returns an error message if a given value is invalid.
  Validation errors are displayed to the user when the form is submitted
  if \`validationBehavior="native"\`. For realtime validation, use the \`isInvalid\`
  prop instead.
- validationBehavior: (union) Whether to use native HTML form validation to prevent form submission
  when the value is missing or invalid, or mark the field as required
  or invalid via ARIA.

***

## Examples

### Controlled Date Picker

Manage and store the value of the date picker in a state variable for controlled behavior. This is also useful for form integration.

```tsx
"use client"

import { DatePicker } from "@medusajs/ui"
import { useState } from "react"

export default function DatePickerControlled() {
  const [date, setDate] = useState<Date | null>(new Date())

  return (
    <div className="space-y-4 w-[300px]">
      <DatePicker
        value={date}
        onChange={setDate}
        aria-label="Select a date"
      />
      <div className="text-ui-fg-subtle text-ui-body-small">
        Selected date: {date ? date.toLocaleDateString() : "None"}
      </div>
    </div>
  )
}

```

### Date Picker With Time

Enable time selection with different granularity levels for precise scheduling.

```tsx
"use client"

import { DatePicker } from "@medusajs/ui"

export default function DatePickerWithTime() {
  return (
    <div className="w-[300px]">
      <DatePicker
        granularity="minute"
        defaultValue={new Date()}
        aria-label="Select date and time"
      />
    </div>
  )
}

```

### Date Picker Min/Max Values

Restrict date selection to a specific range by setting minimum and maximum values.

In the example below, you can only select dates within the next 30 days. Dates outside the range are disabled.

```tsx
"use client"

import { DatePicker } from "@medusajs/ui"

export default function DatePickerMinMax() {
  const today = new Date()
  const maxDate = new Date()
  maxDate.setDate(today.getDate() + 30) // 30 days from today

  return (
    <div className="w-[300px]">
      <DatePicker
        minValue={today}
        maxValue={maxDate}
        aria-label="Select a date within the next 30 days"
      />
    </div>
  )
}

```

### Date Picker Disabled Dates

Disable specific dates like weekends and holidays to prevent selection of unavailable dates.

The example below disables weekends and holidays like Christmas.

```tsx
"use client"

import { DatePicker } from "@medusajs/ui"

export default function DatePickerBusinessHours() {
  return (
    <div className="w-[300px]">
      <DatePicker
        granularity="hour"
        defaultValue={new Date()}
        aria-label="Select date and hour for business scheduling"
        isDateUnavailable={(date) => {
          // Disable weekends and holidays
          const day = date.getDay()
          const isWeekend = day === 0 || day === 6
          
          // Example: Disable specific holiday (Christmas)
          const isChristmas = date.getMonth() === 11 && date.getDate() === 25
          
          return isWeekend || isChristmas
        }}
      />
    </div>
  )
}

```

### Date Picker Granularity Options

Different levels of time precision from date-only to second-precision selection.

```tsx
"use client"

import { DatePicker } from "@medusajs/ui"

export default function DatePickerGranularity() {
  const defaultDate = new Date()

  return (
    <div className="space-y-6 max-w-md">
      <div className="space-y-2">
        <div className="text-ui-fg-base text-ui-body-small font-medium">Date Only</div>
        <DatePicker
          granularity="day"
          defaultValue={defaultDate}
          aria-label="Select day only"
        />
      </div>
      
      <div className="space-y-2">
        <div className="text-ui-fg-base text-ui-body-small font-medium">Date and Time with Hour Precision</div>
        <DatePicker
          granularity="hour"
          defaultValue={defaultDate}
          aria-label="Select date and hour"
        />
      </div>
      
      <div className="space-y-2">
        <div className="text-ui-fg-base text-ui-body-small font-medium">Date and Time with Minute Precision</div>
        <DatePicker
          granularity="minute"
          defaultValue={defaultDate}
          aria-label="Select date and time with minutes"
        />
      </div>
      
      <div className="space-y-2">
        <div className="text-ui-fg-base text-ui-body-small font-medium">Date and Time with Second Precision</div>
        <DatePicker
          granularity="second"
          defaultValue={defaultDate}
          aria-label="Select date and time with seconds"
        />
      </div>
    </div>
  )
}

```

### Date Picker Form Integration

The following example shows how to use the date picker in a form, with simulated form submission.

```tsx
"use client"

import { DatePicker, Button, Label } from "@medusajs/ui"
import { useState } from "react"

export default function DatePickerForm() {
  const [eventDate, setEventDate] = useState<Date | null>(null)
  const [reminderDate, setReminderDate] = useState<Date | null>(null)
  const [submitted, setSubmitted] = useState(false)

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    setSubmitted(true)
    // Here you would typically send data to your API
    setTimeout(() => setSubmitted(false), 5000)
  }

  const isFormValid = eventDate && reminderDate

  return (
    <div className="p-6 max-w-md border border-ui-border-base rounded-lg bg-ui-bg-base">
      <form onSubmit={handleSubmit} className="space-y-6">
        <div className="space-y-2">
          <h3 className="text-ui-fg-base font-medium">Schedule Event</h3>
          <p className="text-ui-fg-subtle text-ui-body-small">
            Set up your event and reminder dates
          </p>
        </div>
        
        <div className="space-y-4">
          <div className="space-y-2">
            <Label htmlFor="event-date">Event Date & Time</Label>
            <DatePicker
              id="event-date"
              value={eventDate}
              onChange={setEventDate}
              minValue={new Date()}
              aria-label="Select event date and time"
            />
          </div>

          <div className="space-y-2">
            <Label htmlFor="reminder-date">Reminder Date</Label>
            <DatePicker
              id="reminder-date"
              value={reminderDate}
              onChange={setReminderDate}
              minValue={new Date()}
              maxValue={eventDate || undefined}
              aria-label="Select reminder date"
            />
          </div>
        </div>

        <Button 
          type="submit" 
          disabled={!isFormValid || submitted}
          className="w-full"
        >
          {submitted ? "Event Scheduled!" : "Schedule Event"}
        </Button>
      </form>

      {submitted && (
        <div className="mt-4 text-ui-fg-subtle text-ui-body-small">
          Event scheduled for {eventDate?.toLocaleString()} with a reminder on {reminderDate?.toLocaleDateString()}.
        </div>
      )}
    </div>
  )
}

```


# Drawer

A component for rendering a sliding panel that overlays the main content.

In this guide, you'll learn how to use the Drawer component.

```tsx
import { Button, Drawer, Text } from "@medusajs/ui"

export default function DrawerDemo() {
  return (
    <Drawer>
      <Drawer.Trigger asChild>
        <Button>Edit Variant</Button>
      </Drawer.Trigger>
      <Drawer.Content>
        <Drawer.Header>
          <Drawer.Title>Edit Variant</Drawer.Title>
        </Drawer.Header>
        <Drawer.Body className="p-4">
          <Text>This is where you edit the variant&apos;s details</Text>
        </Drawer.Body>
        <Drawer.Footer>
          <Drawer.Close asChild>
            <Button variant="secondary">Cancel</Button>
          </Drawer.Close>
          <Button>Save</Button>
        </Drawer.Footer>
      </Drawer.Content>
    </Drawer>
  )
}

```

## Usage

```tsx
import { Drawer } from "@medusajs/ui"
```

```tsx
<Drawer>
  <Drawer.Trigger>Trigger</Drawer.Trigger>
  <Drawer.Content>
    <Drawer.Header>
      <Drawer.Title>Drawer Title</Drawer.Title>
    </Drawer.Header>
    <Drawer.Body>Body</Drawer.Body>
    <Drawer.Footer>Footer</Drawer.Footer>
  </Drawer.Content>
</Drawer>
```

***

## API Reference

### Drawer Props

This component is based on the \[Radix UI Dialog]\(https://www.radix-ui.com/primitives/docs/components/dialog) primitives.



### Drawer.Trigger Props

This component is used to create the trigger button that opens the drawer.
It accepts props from the \[Radix UI Dialog Trigger]\(https://www.radix-ui.com/primitives/docs/components/dialog#trigger) component.



### Drawer.Content Props

This component wraps the content of the drawer.
It accepts props from the \[Radix UI Dialog Content]\(https://www.radix-ui.com/primitives/docs/components/dialog#content) component.

- overlayProps: (ReactComponentPropsWithoutRef) Props for the overlay component.
  It accepts props from the \[Radix UI Dialog Overlay]\(https://www.radix-ui.com/primitives/docs/components/dialog#overlay) component.
- portalProps: (ReactComponentPropsWithoutRef) Props for the portal component that wraps the drawer content.
  It accepts props from the \[Radix UI Dialog Portal]\(https://www.radix-ui.com/primitives/docs/components/dialog#portal) component.

### Drawer.Header Props

This component is used to wrap the header content of the drawer.
This component is based on the \`div\` element and supports all of its props.



### Drawer.Title Props

This component adds an accessible title to the drawer.
It accepts props from the \[Radix UI Dialog Title]\(https://www.radix-ui.com/primitives/docs/components/dialog#title) component.



### Drawer.Body Props

This component is used to wrap the body content of the drawer.
This component is based on the \`div\` element and supports all of its props



### Drawer.Footer Props

This component is used to wrap the footer content of the drawer.
This component is based on the \`div\` element and supports all of its props.



***

## Examples

### Drawer with Form

This example shows a simple form inside a Drawer, demonstrating how to use form elements and handle submission.

```tsx
import { useState } from "react"
import { Button, Drawer, Input, Label } from "@medusajs/ui"

export default function DrawerWithForm() {
  const [name, setName] = useState("")
  const [open, setOpen] = useState(false)
  const [submitted, setSubmitted] = useState(false)

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    setSubmitted(true)
    setOpen(false)
  }

  return (
    <div className="flex flex-col gap-2 items-center">
      <Drawer open={open} onOpenChange={setOpen}>
        <Drawer.Trigger asChild>
          <Button>Open Drawer</Button>
        </Drawer.Trigger>
        <Drawer.Content>
          <Drawer.Header>
            <Drawer.Title>Simple Form</Drawer.Title>
          </Drawer.Header>
          <form onSubmit={handleSubmit} className="flex flex-col flex-1">
            <Drawer.Body>
              <Label htmlFor="name">Name</Label>
              <Input
                id="name"
                value={name}
                onChange={(e) => setName(e.target.value)}
                placeholder="Enter your name"
              />
            </Drawer.Body>
            <Drawer.Footer>
              <Drawer.Close asChild>
                <Button variant="secondary" type="button">
                  Cancel
                </Button>
              </Drawer.Close>
              <Button type="submit">Submit</Button>
            </Drawer.Footer>
          </form>
        </Drawer.Content>
      </Drawer>
      {submitted && (
        <div className="text-ui-fg-muted">Form submitted with name {name}</div>
      )}
    </div>
  )
}

```


# Dropdown Menu

A component for rendering dropdown menus that display a set of actions or options to users.

In this guide, you'll learn how to use the Dropdown Menu component.

```tsx
import { EllipsisHorizontal, PencilSquare, Plus, Trash } from "@medusajs/icons"
import { DropdownMenu, IconButton } from "@medusajs/ui"

export default function DropdownMenuDemo() {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger asChild>
        <IconButton>
          <EllipsisHorizontal />
        </IconButton>
      </DropdownMenu.Trigger>
      <DropdownMenu.Content>
        <DropdownMenu.Item className="gap-x-2">
          <PencilSquare className="text-ui-fg-subtle" />
          Edit
        </DropdownMenu.Item>
        <DropdownMenu.Item className="gap-x-2">
          <Plus className="text-ui-fg-subtle" />
          Add
        </DropdownMenu.Item>
        <DropdownMenu.Separator />
        <DropdownMenu.Item className="gap-x-2">
          <Trash className="text-ui-fg-subtle" />
          Delete
        </DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu>
  )
}

```

## Usage

```tsx
import { DropdownMenu } from "@medusajs/ui"
```

```tsx
<DropdownMenu>
  <DropdownMenu.Trigger>Trigger</DropdownMenu.Trigger>
  <DropdownMenu.Content>
    <DropdownMenu.Item>Edit</DropdownMenu.Item>
    <DropdownMenu.Item>Add</DropdownMenu.Item>
    <DropdownMenu.Item>Delete</DropdownMenu.Item>
  </DropdownMenu.Content>
</DropdownMenu>
```

***

## API Reference

### DropdownMenu Props

This component is based on the \[Radix UI Dropdown Menu]\(https://www.radix-ui.com/primitives/docs/components/dropdown-menu) primitive.



### DropdownMenu.Trigger Props

This component is based on the \[Radix UI Dropdown Menu Trigger]\(https://www.radix-ui.com/primitives/docs/components/dropdown-menu#trigger) primitive.



### DropdownMenu.Content Props

This component is based on the \[Radix UI Dropdown Menu Content]\(https://www.radix-ui.com/primitives/docs/components/dropdown-menu#content) primitive.

- sideOffset: (undefined) The space in pixels between the dropdown menu and its trigger element. Default: 8
- collisionPadding: (undefined) The distance in pixels from the boundary edges where collision detection should occur. Default: 8
- align: (undefined) The alignment of the dropdown menu relative to its trigger element.

  @defaultValue center Default: "center"

### DropdownMenu.Item Props

This component is based on the \[Radix UI Dropdown Menu Item]\(https://www.radix-ui.com/primitives/docs/components/dropdown-menu#item) primitive.



### DropdownMenu.Shortcut Props

This component is based on the \`span\` element and supports all of its props



### DropdownMenu.Hint Props

This component is based on the \`span\` element and supports all of its props



### DropdownMenu.RadioGroup Props

This component is based on the \[Radix UI Dropdown Menu RadioGroup]\(https://www.radix-ui.com/primitives/docs/components/dropdown-menu#radiogroup) primitive.



### DropdownMenu.RadioItem Props

This component is based on the \[Radix UI Dropdown Menu RadioItem]\(https://www.radix-ui.com/primitives/docs/components/dropdown-menu#radioitem) primitive.



***

## Examples

### Sorting

This example shows how to display collection sorting choices using a Dropdown Menu.

```tsx
import { EllipsisHorizontal } from "@medusajs/icons"
import { DropdownMenu, IconButton } from "@medusajs/ui"
import React from "react"

type SortingState = "asc" | "desc" | "alpha" | "alpha-reverse" | "none"

export default function DropdownMenuSorting() {
  const [sort, setSort] = React.useState<SortingState>("none")

  return (
    <div className="flex flex-col items-center gap-y-2">
      <DropdownMenu>
        <DropdownMenu.Trigger asChild>
          <IconButton>
            <EllipsisHorizontal />
          </IconButton>
        </DropdownMenu.Trigger>
        <DropdownMenu.Content className="w-[300px]">
          <DropdownMenu.RadioGroup
            value={sort}
            onValueChange={(v) => setSort(v as SortingState)}
          >
            <DropdownMenu.RadioItem value="none">
              No Sorting
            </DropdownMenu.RadioItem>
            <DropdownMenu.Separator />
            <DropdownMenu.RadioItem value="alpha">
              Alphabetical
              <DropdownMenu.Hint>A-Z</DropdownMenu.Hint>
            </DropdownMenu.RadioItem>
            <DropdownMenu.RadioItem value="alpha-reverse">
              Reverse Alphabetical
              <DropdownMenu.Hint>Z-A</DropdownMenu.Hint>
            </DropdownMenu.RadioItem>
            <DropdownMenu.RadioItem value="asc">
              Created At - Ascending
              <DropdownMenu.Hint>1 - 30</DropdownMenu.Hint>
            </DropdownMenu.RadioItem>
            <DropdownMenu.RadioItem value="desc">
              Created At - Descending
              <DropdownMenu.Hint>30 - 1</DropdownMenu.Hint>
            </DropdownMenu.RadioItem>
          </DropdownMenu.RadioGroup>
        </DropdownMenu.Content>
      </DropdownMenu>
      <span className="txt-small text-ui-fg-muted">Sorting: {sort}</span>
    </div>
  )
}

```

### Dropdown with Submenu

```tsx
import { DropdownMenu, IconButton } from "@medusajs/ui"
import { BarsArrowDown } from "@medusajs/icons"

export default function DropdownMenuSubmenu() {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger asChild>
        <IconButton>
          <BarsArrowDown />
        </IconButton>
      </DropdownMenu.Trigger>
      <DropdownMenu.Content>
        <DropdownMenu.Item>Edit</DropdownMenu.Item>
        <DropdownMenu.SubMenu>
          <DropdownMenu.SubMenuTrigger>
            More Actions
          </DropdownMenu.SubMenuTrigger>
          <DropdownMenu.SubMenuContent>
            <DropdownMenu.Item>Duplicate</DropdownMenu.Item>
            <DropdownMenu.Item>Archive</DropdownMenu.Item>
          </DropdownMenu.SubMenuContent>
        </DropdownMenu.SubMenu>
        <DropdownMenu.Item>Delete</DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu>
  )
}

```

### Disabled Items and Using Icons

```tsx
import { DropdownMenu, IconButton } from "@medusajs/ui"
import { Trash, BarsThree } from "@medusajs/icons"

export default function DropdownMenuDisabledAndIcons() {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger asChild>
        <IconButton>
          <BarsThree />
        </IconButton>
      </DropdownMenu.Trigger>
      <DropdownMenu.Content>
        <DropdownMenu.Item>Edit</DropdownMenu.Item>
        <DropdownMenu.Item disabled>
          <Trash className="mr-2" />
          Delete
        </DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu>
  )
}

```

### Keyboard Shortcuts (with handling)

This example shows how to visually display keyboard shortcuts in the menu and handle them in your application logic.

You can use the <Kbd>{osShortcut}E</Kbd> and <Kbd>{osShortcut}D</Kbd> shortcuts to trigger the actions of the dropdown items.

```tsx
import { useEffect, useCallback } from "react"
import { DropdownMenu, IconButton, toast, Toaster } from "@medusajs/ui"
import { Keyboard } from "@medusajs/icons"

function getOsShortcut() {
  const isMacOs =
    typeof navigator !== "undefined"
      ? navigator.userAgent.toLowerCase().indexOf("mac") !== 0
      : true

  return isMacOs ? "⌘" : "Ctrl"
}

export default function DropdownMenuWithShortcuts() {
  const osShortcut = getOsShortcut()
  const handleEdit = useCallback(() => {
    toast.success("Success", {
      description: "Edit shortcut triggered!",
    })
  }, [])

  const handleDelete = useCallback(() => {
    toast.success("Success", {
      description: "Delete shortcut triggered!",
    })
  }, [])

  useEffect(() => {
    function handleKeydown(e: KeyboardEvent) {
      if (e.metaKey && e.key.toLowerCase() === "e") {
        e.preventDefault()
        handleEdit()
      }
      if (e.metaKey && e.key.toLowerCase() === "d") {
        e.preventDefault()
        handleDelete()
      }
    }
    window.addEventListener("keydown", handleKeydown)
    return () => window.removeEventListener("keydown", handleKeydown)
  }, [handleEdit, handleDelete])

  return (
    <>
      <DropdownMenu>
        <DropdownMenu.Trigger asChild>
          <IconButton>
            <Keyboard />
          </IconButton>
        </DropdownMenu.Trigger>
        <DropdownMenu.Content>
          <DropdownMenu.Item onSelect={handleEdit}>
            Edit
            <DropdownMenu.Shortcut>{osShortcut}E</DropdownMenu.Shortcut>
          </DropdownMenu.Item>
          <DropdownMenu.Item onSelect={handleDelete}>
            Delete
            <DropdownMenu.Shortcut>{osShortcut}D</DropdownMenu.Shortcut>
          </DropdownMenu.Item>
        </DropdownMenu.Content>
      </DropdownMenu>
      <Toaster />
    </>
  )
}

```


# Focus Modal

A component for rendering a modal dialog shown over the main content.

In this guide, you'll learn how to use the Focus Modal component.

```tsx
import { Button, FocusModal, Heading, Input, Label, Text } from "@medusajs/ui"

export default function FocusModalDemo() {
  return (
    <FocusModal>
      <FocusModal.Trigger asChild>
        <Button>Edit Variant</Button>
      </FocusModal.Trigger>
      <FocusModal.Content>
        <FocusModal.Header>
          <FocusModal.Title>Edit Variant</FocusModal.Title>
        </FocusModal.Header>
        <FocusModal.Body className="flex flex-col items-center py-16">
          <div className="flex w-full max-w-lg flex-col gap-y-8">
            <div className="flex flex-col gap-y-1">
              <Heading>Create API key</Heading>
              <Text className="text-ui-fg-subtle">
                Create and manage API keys. You can create multiple keys to
                organize your applications.
              </Text>
            </div>
            <div className="flex flex-col gap-y-2">
              <Label htmlFor="key_name" className="text-ui-fg-subtle">
                Key name
              </Label>
              <Input id="key_name" placeholder="my_app" />
            </div>
          </div>
        </FocusModal.Body>
        <FocusModal.Footer>
          <Button>Save</Button>
        </FocusModal.Footer>
      </FocusModal.Content>
    </FocusModal>
  )
}

```

## Usage

```tsx
import { FocusModal } from "@medusajs/ui"
```

```tsx
<FocusModal>
  <FocusModal.Trigger>Trigger</FocusModal.Trigger>
  <FocusModal.Content>
    <FocusModal.Header>Title</FocusModal.Header>
    <FocusModal.Body>Content</FocusModal.Body>
  </FocusModal.Content>
</FocusModal>
```

***

## API Reference

### FocusModal Props

This component is based on the \[Radix UI Dialog]\(https://www.radix-ui.com/primitives/docs/components/dialog) primitives.

- defaultOpen: (boolean) Whether the modal is opened by default.
- open: (boolean) Whether the modal is opened.
- onOpenChange: (signature) A function to handle when the modal is opened or closed.

### FocusModal.Trigger Props

This component is used to create the trigger button that opens the modal.
It accepts props from the \[Radix UI Dialog Trigger]\(https://www.radix-ui.com/primitives/docs/components/dialog#trigger) component.



### FocusModal.Content Props

This component wraps the content of the modal.
It accepts props from the \[Radix UI Dialog Content]\(https://www.radix-ui.com/primitives/docs/components/dialog#content) component.



### FocusModal.Header Props

This component is used to wrap the header content of the modal.
This component is based on the \`div\` element and supports all of its props



### FocusModal.Body Props

This component is used to wrap the body content of the modal.
This component is based on the \`div\` element and supports all of its props



### FocusModal.Footer Props

This component is used to wrap the footer content of the modal.
This component is based on the \`div\` element and supports all of its props



***

## Examples

### Control Focus Modal Open State

```tsx
import { Button, FocusModal, Heading, Input, Label, Text } from "@medusajs/ui"
import { useState } from "react"

export default function FocusModalControlled() {
  const [open, setOpen] = useState(false)

  return (
    <div>
      <Button onClick={() => setOpen(true)}>Edit Variant</Button>
      <FocusModal open={open} onOpenChange={setOpen}>
        <FocusModal.Content>
          <FocusModal.Header>
            <FocusModal.Title>Edit Variant</FocusModal.Title>
          </FocusModal.Header>
          <FocusModal.Body className="flex flex-col items-center py-16">
            <div className="flex w-full max-w-lg flex-col gap-y-8">
              <div className="flex flex-col gap-y-1">
                <Heading>Create API key</Heading>
                <Text className="text-ui-fg-subtle">
                  Create and manage API keys. You can create multiple keys to
                  organize your applications.
                </Text>
              </div>
              <div className="flex flex-col gap-y-2">
                <Label htmlFor="key_name" className="text-ui-fg-subtle">
                  Key name
                </Label>
                <Input id="key_name" placeholder="my_app" />
              </div>
            </div>
          </FocusModal.Body>
          <FocusModal.Footer>
            <Button onClick={() => setOpen(false)}>Save</Button>
          </FocusModal.Footer>
        </FocusModal.Content>
      </FocusModal>
    </div>
  )
}

```

### Using Form in Focus Modal

```tsx
import { Button, FocusModal, Input, Label } from "@medusajs/ui"
import { useState } from "react"

export default function FocusModalForm() {
  const [open, setOpen] = useState(false)
  const [value, setValue] = useState("")

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    setOpen(false)
  }

  return (
    <div className="flex flex-col gap-2 items-center">
      <FocusModal open={open} onOpenChange={setOpen}>
        <FocusModal.Trigger asChild>
          <Button>Create Item</Button>
        </FocusModal.Trigger>
        <FocusModal.Content>
          <FocusModal.Header>
            <FocusModal.Title>Create Item</FocusModal.Title>
          </FocusModal.Header>
          <form onSubmit={handleSubmit} className="flex flex-col flex-1">
            <FocusModal.Body>
              <div className="p-6">
                <Label htmlFor="name">Name</Label>
                <Input
                  id="name"
                  value={value}
                  onChange={(e) => setValue(e.target.value)}
                  placeholder="Enter your name"
                />
              </div>
            </FocusModal.Body>
            <FocusModal.Footer>
              <Button type="submit">Submit</Button>
            </FocusModal.Footer>
          </form>
        </FocusModal.Content>
      </FocusModal>
      {value && (
        <div className="text-ui-fg-muted">
          Form submitted with name: {value}
        </div>
      )}
    </div>
  )
}

```

### Nested Focus Modals

A focus modal can open another focus modal. These focus modals will be stacked on top of each other. You can nest as many focus modals as you want.

```tsx
import { Button, FocusModal } from "@medusajs/ui"

export default function NestedFocusModals() {
  return (
    <FocusModal>
      <FocusModal.Trigger asChild>
        <Button>Open Outer Modal</Button>
      </FocusModal.Trigger>
      <FocusModal.Content>
        <FocusModal.Header>
          <FocusModal.Title>Outer Modal</FocusModal.Title>
        </FocusModal.Header>
        <FocusModal.Body className="p-6 flex flex-col space-y-2">
          <p>This is the outer modal.</p>
          <FocusModal>
            <FocusModal.Trigger asChild>
              <Button variant="secondary">Open Nested Modal</Button>
            </FocusModal.Trigger>
            <FocusModal.Content>
              <FocusModal.Header>
                <FocusModal.Title>Nested Modal</FocusModal.Title>
              </FocusModal.Header>
              <FocusModal.Body className="p-6">
                <p>This is a nested focus modal for additional information.</p>
              </FocusModal.Body>
            </FocusModal.Content>
          </FocusModal>
        </FocusModal.Body>
      </FocusModal.Content>
    </FocusModal>
  )
}

```


# Heading

A component used for page titles and other headers.

In this guide, you'll learn how to use the Heading component.

```tsx
import { Heading } from "@medusajs/ui"

export default function HeadingDemo() {
  return (
    <div className="flex flex-col items-center">
      <Heading level="h1">This is an H1 heading</Heading>
      <Heading level="h2">This is an H2 heading</Heading>
      <Heading level="h3">This is an H3 heading</Heading>
    </div>
  )
}

```

## Usage

```tsx
import { Heading } from "@medusajs/ui"
```

```tsx
<Heading>A Title</Heading>
```

***

## API Reference

### Heading Props

This component is based on the heading element (\`h1\`, \`h2\`, etc...) depeneding on the specified level
and supports all of its props

- level: (union) The heading level which specifies which heading element is used. Default: "h1"


# Icon Badge

A component that displays an icon in a badge.

In this guide, you'll learn how to use the Icon Badge component.

```tsx
import { BuildingTax } from "@medusajs/icons"
import { IconBadge } from "@medusajs/ui"

export default function IconBadgeDemo() {
  return (
    <IconBadge>
      <BuildingTax />
    </IconBadge>
  )
}

```

## Usage

```tsx
import { IconBadge } from "@medusajs/ui"
import { BuildingTax } from "@medusajs/icons"
```

```tsx
<IconBadge>
  <BuildingTax />
</IconBadge>
```

***

## API Reference

### IconBadge Props

This component is based on the \`span\` element and supports all of its props

- asChild: (boolean) Whether to remove the wrapper \`span\` element and use the
  passed child element instead. Default: false
- color: (union) The badge's color. Default: "grey"
- size: (union) The badge's size. Default: "base"

***

## Examples

### Colors

```tsx
import { IconBadge } from "@medusajs/ui"
import { BuildingTax } from "@medusajs/icons"

export default function IconBadgeAllColors() {
  return (
    <div className="flex gap-3">
      <IconBadge color="grey">
        <BuildingTax />
      </IconBadge>
      <IconBadge color="purple">
        <BuildingTax />
      </IconBadge>
      <IconBadge color="orange">
        <BuildingTax />
      </IconBadge>
      <IconBadge color="red">
        <BuildingTax />
      </IconBadge>
      <IconBadge color="blue">
        <BuildingTax />
      </IconBadge>
      <IconBadge color="green">
        <BuildingTax />
      </IconBadge>
    </div>
  )
}

```

### Sizes

```tsx
import { IconBadge } from "@medusajs/ui"
import { BuildingTax } from "@medusajs/icons"

export default function IconBadgeAllSizes() {
  return (
    <div className="flex gap-3 items-center">
      <IconBadge size="base">
        <BuildingTax />
      </IconBadge>
      <IconBadge size="large">
        <BuildingTax />
      </IconBadge>
    </div>
  )
}

```


# Icon Button

A component that displays an icon in a button.

In this guide, you'll learn how to use the Icon Button component.

```tsx
import { PlusMini } from "@medusajs/icons"
import { IconButton } from "@medusajs/ui"

export default function IconButtonDemo() {
  return (
    <IconButton>
      <PlusMini />
    </IconButton>
  )
}

```

## Usage

```tsx
import { IconButton } from "@medusajs/ui"
import { Plus } from "@medusajs/icons"
```

```tsx
<IconButton>
  <Plus />
</IconButton>
```

***

## API Reference

### IconButton Props

This component is based on the \`button\` element and supports all of its props

- asChild: (boolean) Whether to remove the wrapper \`button\` element and use the
  passed child element instead. Default: false
- isLoading: (boolean) Whether to show a loading spinner. Default: false
- variant: (union) The button's style. Default: "primary"
- size: (union) The button's size. Default: "base"

***

## Examples

### Icon Button Variants

```tsx
import { IconButton } from "@medusajs/ui"
import { PlusMini } from "@medusajs/icons"

export default function IconButtonAllVariants() {
  return (
    <div className="flex gap-2">
      <IconButton variant="primary">
        <PlusMini />
      </IconButton>
      <IconButton variant="transparent">
        <PlusMini />
      </IconButton>
    </div>
  )
}

```

### Icon Button Sizes

```tsx
import { IconButton } from "@medusajs/ui"
import { PlusMini } from "@medusajs/icons"

export default function IconButtonAllSizes() {
  return (
    <div className="flex gap-2 items-center">
      <IconButton size="2xsmall">
        <PlusMini />
      </IconButton>
      <IconButton size="xsmall">
        <PlusMini />
      </IconButton>
      <IconButton size="small">
        <PlusMini />
      </IconButton>
      <IconButton size="base">
        <PlusMini />
      </IconButton>
      <IconButton size="large">
        <PlusMini />
      </IconButton>
      <IconButton size="xlarge">
        <PlusMini />
      </IconButton>
    </div>
  )
}

```

### Icon Button Loading State

```tsx
import { PlusMini } from "@medusajs/icons"
import { IconButton } from "@medusajs/ui"

export default function IconButtonLoading() {
  return (
    <IconButton isLoading className="relative">
      <PlusMini />
    </IconButton>
  )
}

```

### Disabled Icon Button

```tsx
import { PlusMini } from "@medusajs/icons"
import { IconButton } from "@medusajs/ui"

export default function IconButtonDisabled() {
  return (
    <IconButton disabled>
      <PlusMini />
    </IconButton>
  )
}

```


# Inline Tip

A component for displaying a note or tip inline.

In this guide, you'll learn how to use the Inline Tip component.

```tsx
import { InlineTip } from "@medusajs/ui"

export default function InlineTipDemo() {
  return (
    <InlineTip
      label="Tip"
    >
      Medusa UI is a package of React components to be used in Medusa Admin customizations.
    </InlineTip>
  )
}
```

## Usage

```tsx
import { InlineTip } from "@medusajs/ui"
```

```tsx
<InlineTip
  label="This is a tip"
>
  <button>Hover me</button>
</InlineTip>
```

***

## API Reference

### InlineTip Props

This component is based on the \`div\` element and supports all of its props.

- label: (string) The label to display in the tip.
- variant: (union) The variant of the tip. Default: "info"

***

## Examples

### Success Inline Tip

```tsx
import { InlineTip } from "@medusajs/ui"

export default function InlineTipSuccess() {
  return (
    <InlineTip
      label="Success"
      variant="success"
    >
      Product created successfully!
    </InlineTip>
  )
}
```

### Warning Inline Tip

```tsx
import { InlineTip } from "@medusajs/ui"

export default function InlineTipWarning() {
  return (
    <InlineTip
      label="Warning"
      variant="warning"
    >
      This action cannot be undone.
    </InlineTip>
  )
}
```

### Error Inline Tip

```tsx
import { InlineTip } from "@medusajs/ui"

export default function InlineTipError() {
  return (
    <InlineTip
      label="Error"
      variant="error"
    >
      An error occurred. Please try again.
    </InlineTip>
  )
}
```


# Input

A component that renders a form input field using Medusa's design system.

In this guide, you'll learn how to use the Input component.

```tsx
import { Input } from "@medusajs/ui"

export default function InputDemo() {
  return (
    <div className="w-[250px]">
      <Input placeholder="Sales Channel Name" id="sales-channel-name" />
    </div>
  )
}

```

## Usage

```tsx
import { Input } from "@medusajs/ui"
```

```tsx
<Input placeholder="Placeholder" id="input-id" />
```

***

## API Reference

### Input Props

This component is based on the \`input\` element and supports all of its props

- size: (union) The input's size. Default: "base"

***

## Examples

### Password

```tsx
import { Input } from "@medusajs/ui"

export default function InputPassword() {
  return (
    <div className="w-[250px]">
      <Input id="password" type="password" defaultValue="supersecret" />
    </div>
  )
}

```

### Search

```tsx
import { Input } from "@medusajs/ui"

export default function InputSearch() {
  return (
    <div className="w-[250px]">
      <Input placeholder="Search" id="search-input" type="search" />
    </div>
  )
}

```

### Disabled

```tsx
import { Input } from "@medusajs/ui"

export default function InputDisabled() {
  return (
    <div className="w-[250px]">
      <Input placeholder="Disabled" id="disabled-input" disabled />
    </div>
  )
}

```

### Small Size

```tsx
import { Input } from "@medusajs/ui"

export default function InputSmall() {
  return (
    <div className="w-[250px]">
      <Input placeholder="First name" id="first-name" size="small" />
    </div>
  )
}

```

### Controlled

```tsx
import { Input } from "@medusajs/ui"
import { useState } from "react"

export default function InputControlled() {
  const [value, setValue] = useState("")

  return (
    <div className="flex flex-col items-center gap-2">
      <Input
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="Enter name"
        id="controlled-input"
      />
      {value && <span>Hello, {value}!</span>}
    </div>
  )
}

```

### Error State

You can leverage the native `aria-invalid` property to show an error state on your input:

```tsx
import { Input } from "@medusajs/ui"

export default function InputError() {
  return (
    <div className="w-[250px]">
      <Input
        placeholder="Sales Channel Name"
        id="sales-channel-name"
        aria-invalid={true}
      />
    </div>
  )
}

```


# Kbd

A component that renders a badge-styled keyboard (`kbd`) element.

In this guide, you'll learn how to use the Kbd component.

```tsx
import { Kbd } from "@medusajs/ui"

export default function KbdDemo() {
  return <Kbd>⌘ + K</Kbd>
}

```

## Usage

```tsx
import { Kbd } from "@medusajs/ui"
```

```tsx
<Kbd>Ctrl + Shift + A</Kbd>
```

***

## API Reference

### Kbd Props

This component is based on the \`kbd\` element and supports all of its props



# Label

A component that renders an accessible label associated with input fields.

In this guide, you'll learn how to use the Label component.

```tsx
import { Label } from "@medusajs/ui"

export default function LabelDemo() {
  return <Label>Regular label</Label>
}

```

## Usage

```tsx
import { Label } from "@medusajs/ui"
```

```tsx
<Label>Label</Label>
```

***

## API Reference

### Label Props

This component is based on the \[Radix UI Label]\(https://www.radix-ui.com/primitives/docs/components/label) primitive.

- size: (union) The label's size. Default: "base"
- weight: (union) The label's font weight. Default: "regular"

***

## Examples

### Label Sizes

```tsx
import { Label } from "@medusajs/ui"

export default function LabelAllSizes() {
  return (
    <div className="flex gap-8 items-center">
      <div className="flex flex-col gap-1">
        <Label size="xsmall" weight="regular">
          XSmall - Regular
        </Label>
        <Label size="xsmall" weight="plus">
          XSmall - Plus
        </Label>
      </div>
      <div className="flex flex-col gap-1">
        <Label size="small" weight="regular">
          Small - Regular
        </Label>
        <Label size="small" weight="plus">
          Small - Plus
        </Label>
      </div>
      <div className="flex flex-col gap-1">
        <Label size="base" weight="regular">
          Base - Regular
        </Label>
        <Label size="base" weight="plus">
          Base - Plus
        </Label>
      </div>
      <div className="flex flex-col gap-1">
        <Label size="large" weight="regular">
          Large - Regular
        </Label>
        <Label size="large" weight="plus">
          Large - Plus
        </Label>
      </div>
    </div>
  )
}

```

### Label with Form Inputs

```tsx
import { Label, Input } from "@medusajs/ui"
import { Textarea, RadioGroup } from "@medusajs/ui"

export default function LabelWithInputs() {
  return (
    <form className="flex flex-col gap-6">
      <div className="flex flex-col gap-1">
        <Label htmlFor="text-input">Text Input</Label>
        <Input id="text-input" placeholder="Enter text" />
      </div>
      <div className="flex flex-col gap-1">
        <Label htmlFor="checkbox-input">Checkbox</Label>
        <Input id="checkbox-input" type="checkbox" />
      </div>
      <div className="flex flex-col gap-1">
        <Label htmlFor="textarea-input">Textarea</Label>
        <Textarea
          id="textarea-input"
          placeholder="Enter details"
          className="border rounded p-2"
        />
      </div>
      <div className="flex flex-col gap-1">
        <Label>Radio Group</Label>
        <RadioGroup defaultValue="option-1" className="flex gap-4">
          <div className="flex items-center gap-1">
            <RadioGroup.Item id="radio-1" value="option-1" />
            <Label htmlFor="radio-1">Option 1</Label>
          </div>
          <div className="flex items-center gap-1">
            <RadioGroup.Item id="radio-2" value="option-2" />
            <Label htmlFor="radio-2">Option 2</Label>
          </div>
        </RadioGroup>
      </div>
    </form>
  )
}

```


# Progress Accordion

A component that renders a set of expandable content, specifically designed for implementing multi-step tasks.

In this guide, you'll learn how to use the Progress Accordion component.

```tsx
import { ProgressAccordion, Text } from "@medusajs/ui"

export default function ProgressAccordionDemo() {
  return (
    <div className="w-full px-4">
      <ProgressAccordion type="single">
        <ProgressAccordion.Item value="general">
          <ProgressAccordion.Header>General</ProgressAccordion.Header>
          <ProgressAccordion.Content>
            <div className="pb-6">
              <Text size="small">
                Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
                ornare, tortor nec commodo ultrices, diam leo porttitor eros,
                eget ultricies mauris nisl nec nisl. Donec quis magna euismod,
                lacinia ipsum id, varius velit.
              </Text>
            </div>
          </ProgressAccordion.Content>
        </ProgressAccordion.Item>
        <ProgressAccordion.Item value="shipping">
          <ProgressAccordion.Header>Shipping</ProgressAccordion.Header>
          <ProgressAccordion.Content>
            <div className="pb-6">
              <Text size="small">
                Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
                ornare, tortor nec commodo ultrices, diam leo porttitor eros,
                eget ultricies mauris nisl nec nisl. Donec quis magna euismod,
                lacinia ipsum id, varius velit.
              </Text>
            </div>
          </ProgressAccordion.Content>
        </ProgressAccordion.Item>
      </ProgressAccordion>
    </div>
  )
}

```

## Usage

```tsx
import { ProgressAccordion } from "@medusajs/ui"
```

```tsx
<ProgressAccordion type="single">
  <ProgressAccordion.Item value="general">
    <ProgressAccordion.Header>
      General
    </ProgressAccordion.Header>
    <ProgressAccordion.Content>
       {/* Content */}
    </ProgressAccordion.Content>
  </ProgressAccordion.Item>
  <ProgressAccordion.Item value="shipping">
    <ProgressAccordion.Header>
      Shipping
    </ProgressAccordion.Header>
    <ProgressAccordion.Content>
        {/* Content */}
    </ProgressAccordion.Content>
  </ProgressAccordion.Item>
</ProgressAccordion>
```

***

## API Reference

### ProgressAccordion Props

This component is based on the \[Radix UI Accordion]\(https://radix-ui.com/primitives/docs/components/accordion) primitves.



### ProgressAccordion.Header Props

- status: (union) The current status. Default: "not-started"

***

## Examples

### Only One Accordion Open

```tsx
import { ProgressAccordion, Text } from "@medusajs/ui"

export default function ProgressAccordionSingle() {
  return (
    <div className="w-full px-4">
      <ProgressAccordion type="single">
        <ProgressAccordion.Item value="general">
          <ProgressAccordion.Header>General</ProgressAccordion.Header>
          <ProgressAccordion.Content>
            <div className="pb-6">
              <Text size="small">
                Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
                ornare, tortor nec commodo ultrices, diam leo porttitor eros,
                eget ultricies mauris nisl nec nisl. Donec quis magna euismod,
                lacinia ipsum id, varius velit.
              </Text>
            </div>
          </ProgressAccordion.Content>
        </ProgressAccordion.Item>
        <ProgressAccordion.Item value="shipping">
          <ProgressAccordion.Header>Shipping</ProgressAccordion.Header>
          <ProgressAccordion.Content>
            <div className="pb-6">
              <Text size="small">
                Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
                ornare, tortor nec commodo ultrices, diam leo porttitor eros,
                eget ultricies mauris nisl nec nisl. Donec quis magna euismod,
                lacinia ipsum id, varius velit.
              </Text>
            </div>
          </ProgressAccordion.Content>
        </ProgressAccordion.Item>
      </ProgressAccordion>
    </div>
  )
}

```

### Allow Multiple Accordions to Open

```tsx
import { ProgressAccordion, Text } from "@medusajs/ui"

export default function ProgressAccordionSingle() {
  return (
    <div className="w-full px-4">
      <ProgressAccordion type="multiple">
        <ProgressAccordion.Item value="general">
          <ProgressAccordion.Header>General</ProgressAccordion.Header>
          <ProgressAccordion.Content>
            <div className="pb-6">
              <Text size="small">
                Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
                ornare, tortor nec commodo ultrices, diam leo porttitor eros,
                eget ultricies mauris nisl nec nisl. Donec quis magna euismod,
                lacinia ipsum id, varius velit.
              </Text>
            </div>
          </ProgressAccordion.Content>
        </ProgressAccordion.Item>
        <ProgressAccordion.Item value="shipping">
          <ProgressAccordion.Header>Shipping</ProgressAccordion.Header>
          <ProgressAccordion.Content>
            <div className="pb-6">
              <Text size="small">
                Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
                ornare, tortor nec commodo ultrices, diam leo porttitor eros,
                eget ultricies mauris nisl nec nisl. Donec quis magna euismod,
                lacinia ipsum id, varius velit.
              </Text>
            </div>
          </ProgressAccordion.Content>
        </ProgressAccordion.Item>
      </ProgressAccordion>
    </div>
  )
}

```

### Set Status Indicator

```tsx
import { ProgressAccordion, Text } from "@medusajs/ui"

export default function ProgressAccordionStatus() {
  return (
    <div className="w-full px-4">
      <ProgressAccordion type="single">
        <ProgressAccordion.Item value="general">
          <ProgressAccordion.Header status="not-started">
            General
          </ProgressAccordion.Header>
          <ProgressAccordion.Content>
            <div className="pb-6">
              <Text size="small">This step has not started yet.</Text>
            </div>
          </ProgressAccordion.Content>
        </ProgressAccordion.Item>
        <ProgressAccordion.Item value="shipping">
          <ProgressAccordion.Header status="in-progress">
            Shipping
          </ProgressAccordion.Header>
          <ProgressAccordion.Content>
            <div className="pb-6">
              <Text size="small">This step is in progress.</Text>
            </div>
          </ProgressAccordion.Content>
        </ProgressAccordion.Item>
        <ProgressAccordion.Item value="payment">
          <ProgressAccordion.Header status="completed">
            Payment
          </ProgressAccordion.Header>
          <ProgressAccordion.Content>
            <div className="pb-6">
              <Text size="small">This step is completed.</Text>
            </div>
          </ProgressAccordion.Content>
        </ProgressAccordion.Item>
      </ProgressAccordion>
    </div>
  )
}

```

### Controlled Accordion Open State

```tsx
import { ProgressAccordion, Text, Button } from "@medusajs/ui"
import * as React from "react"

export default function ProgressAccordionControlled() {
  const [open, setOpen] = React.useState<string>("general")
  const steps = ["general", "shipping", "payment"]
  const currentIndex = steps.indexOf(open)

  const handleNext = () => {
    if (currentIndex < steps.length - 1) {
      setOpen(steps[currentIndex + 1])
    }
  }
  const handlePrev = () => {
    if (currentIndex > 0) {
      setOpen(steps[currentIndex - 1])
    }
  }

  return (
    <div className="w-full px-4 flex flex-col gap-4">
      <ProgressAccordion type="single" value={open} onValueChange={setOpen}>
        <ProgressAccordion.Item value="general">
          <ProgressAccordion.Header>General</ProgressAccordion.Header>
          <ProgressAccordion.Content>
            <div className="pb-6 flex flex-col gap-2">
              <Text size="small">This is the General step.</Text>
            </div>
          </ProgressAccordion.Content>
        </ProgressAccordion.Item>
        <ProgressAccordion.Item value="shipping">
          <ProgressAccordion.Header>Shipping</ProgressAccordion.Header>
          <ProgressAccordion.Content>
            <div className="pb-6 flex flex-col gap-2">
              <Text size="small">This is the Shipping step.</Text>
            </div>
          </ProgressAccordion.Content>
        </ProgressAccordion.Item>
        <ProgressAccordion.Item value="payment">
          <ProgressAccordion.Header>Payment</ProgressAccordion.Header>
          <ProgressAccordion.Content>
            <div className="pb-6 flex flex-col gap-2">
              <Text size="small">This is the Payment step.</Text>
            </div>
          </ProgressAccordion.Content>
        </ProgressAccordion.Item>
      </ProgressAccordion>
      <div className="mt-4 flex gap-2 self-end">
        <Button
          variant="secondary"
          onClick={handlePrev}
          disabled={currentIndex === 0}
        >
          Prev
        </Button>
        <Button
          onClick={handleNext}
          disabled={currentIndex === steps.length - 1}
        >
          Next
        </Button>
      </div>
    </div>
  )
}

```

### Disabled Accordion Item

```tsx
import { ProgressAccordion, Text } from "@medusajs/ui"

export default function ProgressAccordionDisabled() {
  return (
    <div className="w-full px-4">
      <ProgressAccordion type="single">
        <ProgressAccordion.Item value="general">
          <ProgressAccordion.Header>General</ProgressAccordion.Header>
          <ProgressAccordion.Content>
            <div className="pb-6">
              <Text size="small">This step is enabled.</Text>
            </div>
          </ProgressAccordion.Content>
        </ProgressAccordion.Item>
        <ProgressAccordion.Item value="shipping" disabled>
          <ProgressAccordion.Header>Shipping</ProgressAccordion.Header>
          <ProgressAccordion.Content>
            <div className="pb-6">
              <Text size="small">
                This step is disabled and cannot be opened.
              </Text>
            </div>
          </ProgressAccordion.Content>
        </ProgressAccordion.Item>
      </ProgressAccordion>
    </div>
  )
}

```


# Progress Tabs

A component that renders tabbed content, specifically designed for implementing multi-step tasks.

In this guide, you'll learn how to use the Progress Tabs component.

```tsx
import { ProgressTabs, Text } from "@medusajs/ui"

export default function ProgressTabsDemo() {
  return (
    <div className="w-full px-4">
      <ProgressTabs defaultValue="general">
        <div className="border-b border-ui-border-base">
          <ProgressTabs.List>
            <ProgressTabs.Trigger value="general">General</ProgressTabs.Trigger>
            <ProgressTabs.Trigger value="shipping">
              Shipping
            </ProgressTabs.Trigger>
            <ProgressTabs.Trigger value="payment">Payment</ProgressTabs.Trigger>
          </ProgressTabs.List>
        </div>
        <div className="mt-2">
          <ProgressTabs.Content value="general">
            <Text size="small">
              At ACME, we&apos;re dedicated to providing you with an exceptional
              shopping experience. Our wide selection of products caters to your
              every need, from fashion to electronics and beyond. We take pride
              in our commitment to quality, customer satisfaction, and timely
              delivery. Our friendly customer support team is here to assist you
              with any inquiries or concerns you may have. Thank you for
              choosing ACME as your trusted online shopping destination.
            </Text>
          </ProgressTabs.Content>
          <ProgressTabs.Content value="shipping">
            <Text size="small">
              Shipping is a crucial part of our service, designed to ensure your
              products reach you quickly and securely. Our dedicated team works
              tirelessly to process orders, carefully package items, and
              coordinate with reliable carriers to deliver your purchases to
              your doorstep. We take pride in our efficient shipping process,
              guaranteeing your satisfaction with every delivery.
            </Text>
          </ProgressTabs.Content>
          <ProgressTabs.Content value="payment">
            <Text size="small">
              Our payment process is designed to make your shopping experience
              smooth and secure. We offer a variety of payment options to
              accommodate your preferences, from credit and debit cards to
              online payment gateways. Rest assured that your financial
              information is protected through advanced encryption methods.
              Shopping with us means you can shop with confidence, knowing your
              payments are safe and hassle-free.
            </Text>
          </ProgressTabs.Content>
        </div>
      </ProgressTabs>
    </div>
  )
}

```

## Usage

```tsx
import { ProgressTabs } from "@medusajs/ui"
```

```tsx
<ProgressTabs defaultValue="general">
  <ProgressTabs.List>
    <ProgressTabs.Trigger value="general">
      General
    </ProgressTabs.Trigger>
    <ProgressTabs.Trigger value="shipping">
      Shipping
    </ProgressTabs.Trigger>
    <ProgressTabs.Trigger value="payment">
      Payment
    </ProgressTabs.Trigger>
  </ProgressTabs.List>
  <ProgressTabs.Content value="general">
    {/* Content */}
  </ProgressTabs.Content>
  <ProgressTabs.Content value="shipping">
    {/* Content */}
  </ProgressTabs.Content>
  <ProgressTabs.Content value="payment">
    {/* Content */}
  </ProgressTabs.Content>
</ProgressTabs>
```

***

## API Reference

### ProgressTabs Props

This component is based on the \[Radix UI Tabs]\(https://radix-ui.com/primitives/docs/components/tabs) primitves.

- activationMode: (union) Whether a tab is activated automatically or manually.
- defaultValue: (string) The value of the tab to select by default, if uncontrolled
- dir: (Direction) The direction of navigation between toolbar items.
- onValueChange: (signature) A function called when a new tab is selected
- orientation: (union) The orientation the tabs are layed out.
  Mainly so arrow navigation is done accordingly (left & right vs. up & down)
- value: (string) The value for the selected tab, if controlled

### ProgressTabs.Trigger Props

- status: (union)  Default: "not-started"

***

## Examples

### Set Status Indicator

```tsx
import { ProgressTabs, Text } from "@medusajs/ui"

export default function ProgressTabsStatus() {
  return (
    <div className="w-full px-4">
      <ProgressTabs defaultValue="general">
        <div className="border-b border-ui-border-base">
          <ProgressTabs.List>
            <ProgressTabs.Trigger value="general" status="completed">
              General
            </ProgressTabs.Trigger>
            <ProgressTabs.Trigger value="shipping" status="in-progress">
              Shipping
            </ProgressTabs.Trigger>
            <ProgressTabs.Trigger value="payment" status="not-started">
              Payment
            </ProgressTabs.Trigger>
          </ProgressTabs.List>
        </div>
        <div className="mt-2">
          <ProgressTabs.Content value="general">
            <Text size="small">General step is completed.</Text>
          </ProgressTabs.Content>
          <ProgressTabs.Content value="shipping">
            <Text size="small">Shipping step is in progress.</Text>
          </ProgressTabs.Content>
          <ProgressTabs.Content value="payment">
            <Text size="small">Payment step has not started.</Text>
          </ProgressTabs.Content>
        </div>
      </ProgressTabs>
    </div>
  )
}

```

### Controlled Active Tab

```tsx
import { ProgressTabs, Text, Button } from "@medusajs/ui"
import * as React from "react"

export default function ProgressTabsControlled() {
  const steps = ["general", "shipping", "payment"]
  const [active, setActive] = React.useState("general")
  const currentIndex = steps.indexOf(active)

  const handleNext = () => {
    if (currentIndex < steps.length - 1) {
      setActive(steps[currentIndex + 1])
    }
  }
  const handlePrev = () => {
    if (currentIndex > 0) {
      setActive(steps[currentIndex - 1])
    }
  }

  return (
    <div className="w-full px-4 flex flex-col gap-4">
      <ProgressTabs value={active} onValueChange={setActive}>
        <div className="border-b border-ui-border-base">
          <ProgressTabs.List>
            <ProgressTabs.Trigger value="general">General</ProgressTabs.Trigger>
            <ProgressTabs.Trigger value="shipping">
              Shipping
            </ProgressTabs.Trigger>
            <ProgressTabs.Trigger value="payment">Payment</ProgressTabs.Trigger>
          </ProgressTabs.List>
        </div>
        <div className="mt-2">
          <ProgressTabs.Content value="general">
            <Text size="small">This is the General step.</Text>
          </ProgressTabs.Content>
          <ProgressTabs.Content value="shipping">
            <Text size="small">This is the Shipping step.</Text>
          </ProgressTabs.Content>
          <ProgressTabs.Content value="payment">
            <Text size="small">This is the Payment step.</Text>
          </ProgressTabs.Content>
        </div>
      </ProgressTabs>
      <div className="mt-4 flex gap-2 self-end">
        <Button
          variant="secondary"
          onClick={handlePrev}
          disabled={currentIndex === 0}
        >
          Prev
        </Button>
        <Button
          onClick={handleNext}
          disabled={currentIndex === steps.length - 1}
        >
          Next
        </Button>
      </div>
    </div>
  )
}

```

### Disabled Tab

```tsx
import { ProgressTabs, Text } from "@medusajs/ui"

export default function ProgressTabsDisabled() {
  return (
    <div className="w-full px-4">
      <ProgressTabs defaultValue="general">
        <div className="border-b border-ui-border-base">
          <ProgressTabs.List>
            <ProgressTabs.Trigger value="general">General</ProgressTabs.Trigger>
            <ProgressTabs.Trigger value="shipping" disabled>
              Shipping
            </ProgressTabs.Trigger>
            <ProgressTabs.Trigger value="payment">Payment</ProgressTabs.Trigger>
          </ProgressTabs.List>
        </div>
        <div className="mt-2">
          <ProgressTabs.Content value="general">
            <Text size="small">This is the General step.</Text>
          </ProgressTabs.Content>
          <ProgressTabs.Content value="shipping">
            <Text size="small">This is the Shipping step (disabled).</Text>
          </ProgressTabs.Content>
          <ProgressTabs.Content value="payment">
            <Text size="small">This is the Payment step.</Text>
          </ProgressTabs.Content>
        </div>
      </ProgressTabs>
    </div>
  )
}

```


# Prompt

A component that displays a dialog prompting the user for their approval. It's useful when confirming destructive actions.

This component is useful if you want to control the prompt's content, format, and design. For a simpler approach that follows Medusa's prompt format, refer to the [usePrompt hook](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/ui/app/hooks/use-prompt/index.html.md).

In this guide, you'll learn how to use the Prompt component.

```tsx
import { Button, Prompt } from "@medusajs/ui"

export default function PromptDemo() {
  return (
    <Prompt>
      <Prompt.Trigger asChild>
        <Button>Open</Button>
      </Prompt.Trigger>
      <Prompt.Content>
        <Prompt.Header>
          <Prompt.Title>Delete something</Prompt.Title>
          <Prompt.Description>
            Are you sure? This cannot be undone.
          </Prompt.Description>
        </Prompt.Header>
        <Prompt.Footer>
          <Prompt.Cancel>Cancel</Prompt.Cancel>
          <Prompt.Action>Delete</Prompt.Action>
        </Prompt.Footer>
      </Prompt.Content>
    </Prompt>
  )
}

```

## Usage

```tsx
import { Prompt } from "@medusajs/ui"
```

```tsx
<Prompt>
  <Prompt.Trigger>Trigger</Prompt.Trigger>
  <Prompt.Content>
    <Prompt.Header>
      <Prompt.Title>Title</Prompt.Title>
      <Prompt.Description>Description</Prompt.Description>
    </Prompt.Header>
    <Prompt.Footer>
      <Prompt.Cancel>Cancel</Prompt.Cancel>
      <Prompt.Action>Delete</Prompt.Action>
    </Prompt.Footer>
  </Prompt.Content>
</Prompt>
```

***

## API Reference

### Prompt Props

This component is based on the \[Radix UI Alert Dialog]\(https://www.radix-ui.com/primitives/docs/components/alert-dialog) primitives.

- variant: (union) The variant of the prompt. Default: "danger"

### Prompt.Header Props

This component is based on the \`div\` element and supports all of its props



### Prompt.Footer Props

This component is based on the \`div\` element and supports all of its props



***

## Examples

### Confirmation Prompt Variant

The `confirmation` variant is useful when confirming an operation that isn't destructive, such as deleting an item.

```tsx
import { Button, Prompt } from "@medusajs/ui"

export default function PromptConfirmation() {
  return (
    <Prompt variant="confirmation">
      <Prompt.Trigger asChild>
        <Button>Open Confirmation</Button>
      </Prompt.Trigger>
      <Prompt.Content>
        <Prompt.Header>
          <Prompt.Title>Confirm Action</Prompt.Title>
          <Prompt.Description>
            Are you sure you want to proceed? This action can be undone.
          </Prompt.Description>
        </Prompt.Header>
        <Prompt.Footer>
          <Prompt.Cancel>Cancel</Prompt.Cancel>
          <Prompt.Action>Confirm</Prompt.Action>
        </Prompt.Footer>
      </Prompt.Content>
    </Prompt>
  )
}

```


# Radio Group

A component that renders a group of radio buttons using Medusa's design system.

In this guide, you'll learn how to use the Radio Group component.

```tsx
import { Label, RadioGroup } from "@medusajs/ui"

export default function RadioGroupDemo() {
  return (
    <RadioGroup>
      <div className="flex items-center gap-x-3">
        <RadioGroup.Item value="1" id="radio_1" />
        <Label htmlFor="radio_1" weight="plus">
          Radio 1
        </Label>
      </div>
      <div className="flex items-center gap-x-3">
        <RadioGroup.Item value="2" id="radio_2" />
        <Label htmlFor="radio_2" weight="plus">
          Radio 2
        </Label>
      </div>
      <div className="flex items-center gap-x-3">
        <RadioGroup.Item value="3" id="radio_3" />
        <Label htmlFor="radio_3" weight="plus">
          Radio 3
        </Label>
      </div>
    </RadioGroup>
  )
}

```

## Usage

```tsx
import { RadioGroup } from "@medusajs/ui"
```

```tsx
<RadioGroup>
  <RadioGroup.Item value="1" id="radio_1" />
  <RadioGroup.Item value="2" id="radio_2" />
  <RadioGroup.Item value="3" id="radio_3" />
</RadioGroup>
```

***

## API Reference

### RadioGroup Props

This component is based on the \[Radix UI Radio Group]\(https://www.radix-ui.com/primitives/docs/components/radio-group) primitives.



***

## Examples

### Radio Group with Descriptions

```tsx
import { Label, RadioGroup, Text } from "@medusajs/ui"

export default function RadioGroupDescriptions() {
  return (
    <RadioGroup>
      <div className="flex items-start gap-x-3">
        <RadioGroup.Item value="1" id="radio_1_descriptions" />
        <div className="flex flex-col gap-y-0.5">
          <Label htmlFor="radio_1_descriptions" weight="plus">
            Radio 1
          </Label>
          <Text className="text-ui-fg-subtle">
            The quick brown fox jumps over the lazy dog.
          </Text>
        </div>
      </div>
      <div className="flex items-start gap-x-3">
        <RadioGroup.Item value="2" id="radio_2_descriptions" />
        <div className="flex flex-col gap-y-0.5">
          <Label htmlFor="radio_2_descriptions" weight="plus">
            Radio 2
          </Label>
          <Text className="text-ui-fg-subtle">
            The quick brown fox jumps over the lazy dog.
          </Text>
        </div>
      </div>
      <div className="flex items-start gap-x-3">
        <RadioGroup.Item value="3" id="radio_3_descriptions" />
        <div className="flex flex-col gap-y-0.5">
          <Label htmlFor="radio_3_descriptions" weight="plus">
            Radio 3
          </Label>
          <Text className="text-ui-fg-subtle">
            The quick brown fox jumps over the lazy dog.
          </Text>
        </div>
      </div>
    </RadioGroup>
  )
}

```

### Controlled Radio Group

```tsx
import { Label, RadioGroup } from "@medusajs/ui"
import * as React from "react"

export default function RadioGroupControlled() {
  const [value, setValue] = React.useState("1")
  return (
    <div className="flex flex-col gap-2 items-center">
      <RadioGroup value={value} onValueChange={setValue}>
        <div className="flex items-center gap-x-3">
          <RadioGroup.Item value="1" id="radio_1_controlled" />
          <Label htmlFor="radio_1_controlled" weight="plus">
            Radio 1
          </Label>
        </div>
        <div className="flex items-center gap-x-3">
          <RadioGroup.Item value="2" id="radio_2_controlled" />
          <Label htmlFor="radio_2_controlled" weight="plus">
            Radio 2
          </Label>
        </div>
        <div className="flex items-center gap-x-3">
          <RadioGroup.Item value="3" id="radio_3_controlled" />
          <Label htmlFor="radio_3_controlled" weight="plus">
            Radio 3
          </Label>
        </div>
      </RadioGroup>
      <div className="txt-small text-ui-fg-muted">Selected value: {value}</div>
    </div>
  )
}

```

### Radio Group with a Disabled Item

```tsx
import { Label, RadioGroup } from "@medusajs/ui"

export default function RadioGroupDisabled() {
  return (
    <RadioGroup>
      <div className="flex items-center gap-x-3">
        <RadioGroup.Item value="1" id="radio_1_disabled" />
        <Label htmlFor="radio_1_disabled" weight="plus">
          Radio 1
        </Label>
      </div>
      <div className="flex items-center gap-x-3">
        <RadioGroup.Item value="2" id="radio_2_disabled" />
        <Label htmlFor="radio_2_disabled" weight="plus">
          Radio 2
        </Label>
      </div>
      <div className="flex items-center gap-x-3">
        <RadioGroup.Item value="3" id="radio_3_disabled" disabled={true} />
        <Label htmlFor="radio_3_disabled" weight="plus">
          Radio 3
        </Label>
      </div>
    </RadioGroup>
  )
}

```

***

## Radio Choice Box

The `RadioGroup.ChoiceBox` component allows you to show a group of radio buttons, each in a box with a label and description.

```tsx
import { RadioGroup } from "@medusajs/ui"

export default function RadioGroupChoiceBox() {
  return (
    <RadioGroup defaultValue="option1">
      <RadioGroup.ChoiceBox
        value="option1"
        label="Option 1"
        description="This is the first option."
      />
      <RadioGroup.ChoiceBox
        value="option2"
        label="Option 2"
        description="This is the second option."
      />
      <RadioGroup.ChoiceBox
        value="option3"
        label="Option 3"
        description="This is the third option."
      />
    </RadioGroup>
  )
}

```

### Choice Box API Reference

### RadioGroup.ChoiceBox Props

This component is based on the \[Radix UI Radio Group Item]\(https://www.radix-ui.com/primitives/docs/components/radio-group#item) primitives.

- label: (string) The label for the radio button.
- description: (string) The description for the radio button.
- value: (string) The value of the radio button.


# Select

A component that displays a select form input using Medusa's design system.

In this guide, you'll learn how to use the Select component.

```tsx
import { Select } from "@medusajs/ui"

export default function SelectDemo() {
  return (
    <div className="w-[256px]">
      <Select>
        <Select.Trigger>
          <Select.Value placeholder="Select a currency" />
        </Select.Trigger>
        <Select.Content>
          {currencies.map((item) => (
            <Select.Item key={item.value} value={item.value}>
              {item.label}
            </Select.Item>
          ))}
        </Select.Content>
      </Select>
    </div>
  )
}

const currencies = [
  {
    value: "eur",
    label: "EUR",
  },
  {
    value: "usd",
    label: "USD",
  },
  {
    value: "dkk",
    label: "DKK",
  },
]

```

## Usage

```tsx
import { Select } from "@medusajs/ui"
```

```tsx
<Select>
  <Select.Trigger>
    <Select.Value placeholder="Placeholder" />
  </Select.Trigger>
  <Select.Content>
    {items.map((item) => (
      <Select.Item key={item.value} value={item.value}>
        {item.label}
      </Select.Item>
    ))}
  </Select.Content>
</Select>
```

***

## API Reference

### Select Props

This component is based on \[Radix UI Select]\(https://www.radix-ui.com/primitives/docs/components/select).
It also accepts all props of the HTML \`select\` component.

- size: (union) The select's size. Default: "base"

### Select.Trigger Props

The trigger that toggles the select.
It's based on \[Radix UI Select Trigger]\(https://www.radix-ui.com/primitives/docs/components/select#trigger).



### Select.Value Props

Displays the selected value, or a placeholder if no value is selected.
It's based on \[Radix UI Select Value]\(https://www.radix-ui.com/primitives/docs/components/select#value).



### Select.Group Props

Groups multiple items together.



### Select.Label Props

Used to label a group of items.
It's based on \[Radix UI Select Label]\(https://www.radix-ui.com/primitives/docs/components/select#label).



### Select.Item Props

An item in the select. It's based on \[Radix UI Select Item]\(https://www.radix-ui.com/primitives/docs/components/select#item)
and accepts its props.



### Select.Content Props

The content that appears when the select is open.
It's based on \[Radix UI Select Content]\(https://www.radix-ui.com/primitives/docs/components/select#content).

- position: (union) Whether to show the select items below (\`popper\`) or over (\`item-aligned\`) the select input. Default: "popper"
- sideOffset: (number) The distance of the content pop-up in pixels from the select input. Only available when position is set to popper. Default: 8
- collisionPadding: (union) The distance in pixels from the boundary edges where collision detection should occur. Only available when position is set to popper. Default: 24

***

## Examples

### Small Select

```tsx
import { Select } from "@medusajs/ui"

export default function SelectSmall() {
  return (
    <div className="w-[256px]">
      <Select size="small">
        <Select.Trigger>
          <Select.Value placeholder="Select a currency" />
        </Select.Trigger>
        <Select.Content>
          {currencies.map((item) => (
            <Select.Item key={item.value} value={item.value}>
              {item.label}
            </Select.Item>
          ))}
        </Select.Content>
      </Select>
    </div>
  )
}

const currencies = [
  {
    value: "eur",
    label: "EUR",
  },
  {
    value: "usd",
    label: "USD",
  },
  {
    value: "dkk",
    label: "DKK",
  },
]

```

### Select Item-Aligned Position

```tsx
import { Select } from "@medusajs/ui"

export default function SelectItemAligned() {
  return (
    <div className="w-[256px]">
      <Select>
        <Select.Trigger>
          <Select.Value placeholder="Select a currency" />
        </Select.Trigger>
        <Select.Content position="item-aligned">
          {currencies.map((item) => (
            <Select.Item key={item.value} value={item.value}>
              {item.label}
            </Select.Item>
          ))}
        </Select.Content>
      </Select>
    </div>
  )
}

const currencies = [
  {
    value: "eur",
    label: "EUR",
  },
  {
    value: "usd",
    label: "USD",
  },
  {
    value: "dkk",
    label: "DKK",
  },
]

```

### Disabled Select

```tsx
import { Select } from "@medusajs/ui"

export default function SelectDemo() {
  return (
    <div className="w-[256px]">
      <Select disabled>
        <Select.Trigger>
          <Select.Value placeholder="Select a currency" />
        </Select.Trigger>
        <Select.Content>
          {currencies.map((item) => (
            <Select.Item key={item.value} value={item.value}>
              {item.label}
            </Select.Item>
          ))}
        </Select.Content>
      </Select>
    </div>
  )
}

const currencies = [
  {
    value: "eur",
    label: "EUR",
  },
  {
    value: "usd",
    label: "USD",
  },
  {
    value: "dkk",
    label: "DKK",
  },
]

```

### Select with Grouped Items

```tsx
import { Select } from "@medusajs/ui"

export default function SelectDemo() {
  return (
    <div className="w-[256px]">
      <Select>
        <Select.Trigger>
          <Select.Value placeholder="Select a currency" />
        </Select.Trigger>
        <Select.Content>
          {data.map((group) => (
            <Select.Group key={group.label}>
              <Select.Label>{group.label}</Select.Label>
              {group.items.map((item) => (
                <Select.Item key={item.value} value={item.value}>
                  {item.label}
                </Select.Item>
              ))}
            </Select.Group>
          ))}
        </Select.Content>
      </Select>
    </div>
  )
}

const data = [
  {
    label: "Shirts",
    items: [
      {
        value: "dress-shirt-solid",
        label: "Solid Dress Shirt",
      },
      {
        value: "dress-shirt-check",
        label: "Check Dress Shirt",
      },
    ],
  },
  {
    label: "T-Shirts",
    items: [
      {
        value: "v-neck",
        label: "V-Neck",
      },
      {
        value: "crew-neck",
        label: "Crew Neck",
      },
      {
        value: "henley",
        label: "Henley",
      },
    ],
  },
]

```

### Controlled Select

```tsx
import { Select } from "@medusajs/ui"
import * as React from "react"

export default function SelectDemo() {
  const [value, setValue] = React.useState<string | undefined>()

  return (
    <div className="w-[256px]">
      <Select onValueChange={setValue} value={value}>
        <Select.Trigger>
          <Select.Value placeholder="Select a currency" />
        </Select.Trigger>
        <Select.Content>
          {currencies.map((item) => (
            <Select.Item key={item.value} value={item.value}>
              {item.label}
            </Select.Item>
          ))}
        </Select.Content>
      </Select>
    </div>
  )
}

const currencies = [
  {
    value: "eur",
    label: "EUR",
  },
  {
    value: "usd",
    label: "USD",
  },
  {
    value: "dkk",
    label: "DKK",
  },
]

```


# Status Badge

A component that displays the status of an item in a badge style. It's useful to indicate states like "Active", "Published", or "Draft".

In this guide, you'll learn how to use the Status Badge component.

```tsx
import { StatusBadge } from "@medusajs/ui"

export default function StatusBadgeDemo() {
  return <StatusBadge>Draft</StatusBadge>
}

```

## Usage

```tsx
import { StatusBadge } from "@medusajs/ui"
```

```tsx
<StatusBadge color="green">Active</StatusBadge>
```

***

## API Reference

### StatusBadge Props

This component is based on the span element and supports all of its props

- color: (union) The status's color. Default: "grey"

***

## Examples

### Status Badge Colors

```tsx
import { StatusBadge } from "@medusajs/ui"

export default function StatusBadgeAllColors() {
  return (
    <div className="flex flex-wrap gap-2">
      <StatusBadge color="green">Active</StatusBadge>
      <StatusBadge color="red">Error</StatusBadge>
      <StatusBadge color="orange">Pending</StatusBadge>
      <StatusBadge color="blue">Info</StatusBadge>
      <StatusBadge color="purple">Archived</StatusBadge>
      <StatusBadge color="grey">Draft</StatusBadge>
    </div>
  )
}

```


# Switch

A component for toggling between two states, typically on and off. It's essentially a checkbox in the form of a switch.

In this guide, you'll learn how to use the Switch component.

```tsx
import { Label, Switch } from "@medusajs/ui"

export default function SwitchDemo() {
  return (
    <div className="flex items-center gap-x-2">
      <Switch id="manage-inventory" />
      <Label htmlFor="manage-inventory">Manage Inventory</Label>
    </div>
  )
}

```

## Usage

```tsx
import { Switch } from "@medusajs/ui"
```

```tsx
<Switch />
```

***

## API Reference

### Switch Props

This component is based on the \[Radix UI Switch]\(https://www.radix-ui.com/primitives/docs/components/switch) primitive.

- size: (union) The switch's size. Default: "base"

***

## Examples

### Switch Sizes

```tsx
import { Label, Switch } from "@medusajs/ui"

export default function SwitchAllSizes() {
  return (
    <div className="flex flex-col gap-y-4">
      <div className="flex items-center gap-x-2">
        <Switch id="switch-small" size="small" />
        <Label htmlFor="switch-small" size="small">
          Small switch
        </Label>
      </div>
      <div className="flex items-center gap-x-2">
        <Switch id="switch-base" size="base" />
        <Label htmlFor="switch-base" size="base">
          Base switch
        </Label>
      </div>
    </div>
  )
}

```

### Controlled Switch

```tsx
import { useState } from "react"
import { Label, Switch } from "@medusajs/ui"

export default function SwitchControlled() {
  const [checked, setChecked] = useState(false)

  return (
    <div className="flex flex-col gap-2">
      <div className="flex items-center gap-x-2">
        <Switch
          id="manage-inventory-controlled"
          checked={checked}
          onCheckedChange={setChecked}
        />
        <Label htmlFor="manage-inventory-controlled">Manage Inventory</Label>
      </div>
      <div className="txt-small text-ui-fg-muted">
        {checked
          ? "You are managing inventory"
          : "You are not managing inventory"}
      </div>
    </div>
  )
}

```

### Disabled Switch

```tsx
import { Label, Switch } from "@medusajs/ui"

export default function SwitchDisabled() {
  return (
    <div className="flex items-center gap-x-2">
      <Switch id="manage-inventory-disabled" disabled={true} />
      <Label htmlFor="manage-inventory-disabled">Manage Inventory</Label>
    </div>
  )
}

```


# Table

A component that displays data in a structured table format.

In this guide, you'll learn how to use the Table component.

If you're looking to add a table to your Medusa Admin customizations with advanced features like filters, search, sorting, and bulk actions, refer to the [DataTable](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/ui/app/components/data-table/index.html.md) component instead.

```tsx
import { Table } from "@medusajs/ui"

type Order = {
  id: string
  displayId: number
  customer: string
  email: string
  amount: number
  currency: string
}

const fakeData: Order[] = [
  {
    id: "order_6782",
    displayId: 86078,
    customer: "Jill Miller",
    email: "32690@gmail.com",
    amount: 493,
    currency: "EUR",
  },
  {
    id: "order_46487",
    displayId: 42845,
    customer: "Sarah Garcia",
    email: "86379@gmail.com",
    amount: 113,
    currency: "JPY",
  },
  {
    id: "order_8169",
    displayId: 39129,
    customer: "Josef Smith",
    email: "89383@gmail.com",
    amount: 43,
    currency: "USD",
  },
  {
    id: "order_67883",
    displayId: 5548,
    customer: "Elvis Jones",
    email: "52860@gmail.com",
    amount: 840,
    currency: "GBP",
  },
  {
    id: "order_61121",
    displayId: 87668,
    customer: "Charles Rodriguez",
    email: "45675@gmail.com",
    amount: 304,
    currency: "GBP",
  },
]

export default function TableDemo() {
  return (
    <Table>
      <Table.Header>
        <Table.Row>
          <Table.HeaderCell>#</Table.HeaderCell>
          <Table.HeaderCell>Customer</Table.HeaderCell>
          <Table.HeaderCell>Email</Table.HeaderCell>
          <Table.HeaderCell className="text-right">Amount</Table.HeaderCell>
          <Table.HeaderCell></Table.HeaderCell>
        </Table.Row>
      </Table.Header>
      <Table.Body>
        {fakeData.map((order) => {
          return (
            <Table.Row
              key={order.id}
              className="[&_td:last-child]:w-[1%] [&_td:last-child]:whitespace-nowrap"
            >
              <Table.Cell>{order.displayId}</Table.Cell>
              <Table.Cell>{order.customer}</Table.Cell>
              <Table.Cell>{order.email}</Table.Cell>
              <Table.Cell className="text-right">
                {new Intl.NumberFormat("en-US", {
                  style: "currency",
                  currency: order.currency,
                }).format(order.amount)}
              </Table.Cell>
              <Table.Cell className="text-ui-fg-muted">
                {order.currency}
              </Table.Cell>
            </Table.Row>
          )
        })}
      </Table.Body>
    </Table>
  )
}

```

## Usage

```tsx
import { Table } from "@medusajs/ui"
```

```tsx
<Table>
  <Table.Header>
    <Table.Row>
      <Table.HeaderCell>#</Table.HeaderCell>
      <Table.HeaderCell>Customer</Table.HeaderCell>
      <Table.HeaderCell>Email</Table.HeaderCell>
    </Table.Row>
  </Table.Header>
  <Table.Body>
    <Table.Row>
      <Table.Cell>1</Table.Cell>
      <Table.Cell>Emil Larsson</Table.Cell>
      <Table.Cell>emil2738@gmail.com</Table.Cell>
    </Table.Row>
  </Table.Body>
</Table>
```

***

## API Reference

### Table Props

This component is based on the table element and its various children:

\- \`Table\`: \`table\`
\- \`Table.Header\`: \`thead\`
\- \`Table.Row\`: \`tr\`
\- \`Table.HeaderCell\`: \`th\`
\- \`Table.Body\`: \`tbody\`
\- \`Table.Cell\`: \`td\`

Each component supports the props or attributes of its equivalent HTML element.



### Table.Pagination Props

This component is based on the \`div\` element and supports all of its props

- count: (number) The total number of items.
- pageSize: (number) The number of items per page.
- pageIndex: (number) The current page index.
- pageCount: (number) The total number of pages.
- canPreviousPage: (boolean) Whether there's a previous page that can be navigated to.
- canNextPage: (boolean) Whether there's a next page that can be navigated to.
- translations: (signature) An optional object of words to use in the pagination component.
  Use this to override the default words, or translate them into another language. Default: \{
  &#x20; of: "of",
  &#x20; results: "results",
  &#x20; pages: "pages",
  &#x20; prev: "Prev",
  &#x20; next: "Next",
  }
- previousPage: (signature) A function that handles navigating to the previous page.
  This function should handle retrieving data for the previous page.
- nextPage: (signature) A function that handles navigating to the next page.
  This function should handle retrieving data for the next page.

***

## Examples

### Table with Pagination

```tsx
import { Table } from "@medusajs/ui"
import { useMemo, useState } from "react"

type Order = {
  id: string
  displayId: number
  customer: string
  email: string
  amount: number
  currency: string
}

export default function TableDemo() {
  const fakeData: Order[] = useMemo(
    () => [
      {
        id: "order_6782",
        displayId: 86078,
        customer: "Jill Miller",
        email: "32690@gmail.com",
        amount: 493,
        currency: "EUR",
      },
      {
        id: "order_46487",
        displayId: 42845,
        customer: "Sarah Garcia",
        email: "86379@gmail.com",
        amount: 113,
        currency: "JPY",
      },
      {
        id: "order_8169",
        displayId: 39129,
        customer: "Josef Smith",
        email: "89383@gmail.com",
        amount: 43,
        currency: "USD",
      },
      {
        id: "order_67883",
        displayId: 5548,
        customer: "Elvis Jones",
        email: "52860@gmail.com",
        amount: 840,
        currency: "GBP",
      },
      {
        id: "order_61121",
        displayId: 87668,
        customer: "Charles Rodriguez",
        email: "45675@gmail.com",
        amount: 304,
        currency: "GBP",
      },
    ],
    []
  )
  const [currentPage, setCurrentPage] = useState(0)
  const pageSize = 3
  const pageCount = Math.ceil(fakeData.length / pageSize)
  const canNextPage = useMemo(
    () => currentPage < pageCount - 1,
    [currentPage, pageCount]
  )
  const canPreviousPage = useMemo(() => currentPage - 1 >= 0, [currentPage])

  const nextPage = () => {
    if (canNextPage) {
      setCurrentPage(currentPage + 1)
    }
  }

  const previousPage = () => {
    if (canPreviousPage) {
      setCurrentPage(currentPage - 1)
    }
  }

  const currentOrders = useMemo(() => {
    const offset = currentPage * pageSize
    const limit = Math.min(offset + pageSize, fakeData.length)

    return fakeData.slice(offset, limit)
  }, [currentPage, pageSize, fakeData])

  return (
    <div className="flex gap-1 flex-col">
      <Table>
        <Table.Header>
          <Table.Row>
            <Table.HeaderCell>#</Table.HeaderCell>
            <Table.HeaderCell>Customer</Table.HeaderCell>
            <Table.HeaderCell>Email</Table.HeaderCell>
            <Table.HeaderCell className="text-right">Amount</Table.HeaderCell>
            <Table.HeaderCell></Table.HeaderCell>
          </Table.Row>
        </Table.Header>
        <Table.Body>
          {currentOrders.map((order) => {
            return (
              <Table.Row
                key={order.id}
                className="[&_td:last-child]:w-[1%] [&_td:last-child]:whitespace-nowrap"
              >
                <Table.Cell>{order.displayId}</Table.Cell>
                <Table.Cell>{order.customer}</Table.Cell>
                <Table.Cell>{order.email}</Table.Cell>
                <Table.Cell className="text-right">
                  {new Intl.NumberFormat("en-US", {
                    style: "currency",
                    currency: order.currency,
                  }).format(order.amount)}
                </Table.Cell>
                <Table.Cell className="text-ui-fg-muted">
                  {order.currency}
                </Table.Cell>
              </Table.Row>
            )
          })}
        </Table.Body>
      </Table>
      <Table.Pagination
        count={fakeData.length}
        pageSize={pageSize}
        pageIndex={currentPage}
        pageCount={fakeData.length}
        canPreviousPage={canPreviousPage}
        canNextPage={canNextPage}
        previousPage={previousPage}
        nextPage={nextPage}
      />
    </div>
  )
}

```


# Tabs

A component that displays tabbed content.

In this guide, you'll learn how to use the Tabs component.

```tsx
import { Tabs, Text } from "@medusajs/ui"

export default function TabsDemo() {
  return (
    <div className="w-full px-4 flex flex-col gap-4">
      <Tabs defaultValue="general">
        <Tabs.List>
          <Tabs.Trigger value="general">General</Tabs.Trigger>
          <Tabs.Trigger value="shipping">Shipping</Tabs.Trigger>
          <Tabs.Trigger value="payment">Payment</Tabs.Trigger>
        </Tabs.List>
        <div className="mt-2">
          <Tabs.Content value="general">
            <Text size="small">
              At ACME, we&apos;re dedicated to providing you with an exceptional
              shopping experience. Our wide selection of products caters to your
              every need, from fashion to electronics and beyond. We take pride
              in our commitment to quality, customer satisfaction, and timely
              delivery. Our friendly customer support team is here to assist you
              with any inquiries or concerns you may have. Thank you for
              choosing ACME as your trusted online shopping destination.
            </Text>
          </Tabs.Content>
          <Tabs.Content value="shipping">
            <Text size="small">
              Shipping is a crucial part of our service, designed to ensure your
              products reach you quickly and securely. Our dedicated team works
              tirelessly to process orders, carefully package items, and
              coordinate with reliable carriers to deliver your purchases to
              your doorstep. We take pride in our efficient shipping process,
              guaranteeing your satisfaction with every delivery.
            </Text>
          </Tabs.Content>
          <Tabs.Content value="payment">
            <Text size="small">
              Our payment process is designed to make your shopping experience
              smooth and secure. We offer a variety of payment options to
              accommodate your preferences, from credit and debit cards to
              online payment gateways. Rest assured that your financial
              information is protected through advanced encryption methods.
              Shopping with us means you can shop with confidence, knowing your
              payments are safe and hassle-free.
            </Text>
          </Tabs.Content>
        </div>
      </Tabs>
      <Text size="xsmall" className="text-ui-fg-muted">
        Use the left and right arrow keys to navigate between the tabs. You must
        focus on a tab first.
      </Text>
    </div>
  )
}

```

## Usage

```tsx
import { Tabs } from "@medusajs/ui"
```

```tsx
<Tabs>
  <Tabs.List>
    <Tabs.Trigger value="1">Tab 1</Tabs.Trigger>
    <Tabs.Trigger value="2">Tab 2</Tabs.Trigger>
    <Tabs.Trigger value="3">Tab 3</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Content value="1">Panel 1</Tabs.Content>
  <Tabs.Content value="2">Panel 2</Tabs.Content>
  <Tabs.Content value="3">Panel 3</Tabs.Content>
</Tabs>
```

***

## API Reference

### Tabs Props

This component is based on the \[Radix UI Tabs]\(https://radix-ui.com/primitives/docs/components/tabs) primitves

- activationMode: (union) Whether a tab is activated automatically or manually.
- defaultValue: (string) The value of the tab to select by default, if uncontrolled
- dir: (Direction) The direction of navigation between toolbar items.
- onValueChange: (signature) A function called when a new tab is selected
- orientation: (union) The orientation the tabs are layed out.
  Mainly so arrow navigation is done accordingly (left & right vs. up & down)
- value: (string) The value for the selected tab, if controlled

***

## Examples

### Controlled Tabs

```tsx
import { Tabs, Text } from "@medusajs/ui"
import { useState } from "react"

export default function TabsControlled() {
  const [value, setValue] = useState("general")
  return (
    <div className="w-full px-4 flex flex-col">
      <Tabs value={value} onValueChange={setValue}>
        <Tabs.List>
          <Tabs.Trigger value="general">General</Tabs.Trigger>
          <Tabs.Trigger value="shipping">Shipping</Tabs.Trigger>
          <Tabs.Trigger value="payment">Payment</Tabs.Trigger>
        </Tabs.List>
        <div className="mt-2">
          <Tabs.Content value="general">
            <Text size="small">This is the General tab (controlled).</Text>
          </Tabs.Content>
          <Tabs.Content value="shipping">
            <Text size="small">This is the Shipping tab (controlled).</Text>
          </Tabs.Content>
          <Tabs.Content value="payment">
            <Text size="small">This is the Payment tab (controlled).</Text>
          </Tabs.Content>
        </div>
      </Tabs>
      <Text size="xsmall" className="text-ui-fg-muted">
        Use the left and right arrow keys to navigate between the tabs. You must
        focus on a tab first.
      </Text>
    </div>
  )
}

```

### Tabs with a Disabled Tab

```tsx
import { Tabs, Text } from "@medusajs/ui"

export default function TabsDisabled() {
  return (
    <div className="w-full px-4 flex flex-col gap-4">
      <Tabs defaultValue="general">
        <Tabs.List>
          <Tabs.Trigger value="general">General</Tabs.Trigger>
          <Tabs.Trigger value="shipping" disabled>
            Shipping (Disabled)
          </Tabs.Trigger>
          <Tabs.Trigger value="payment">Payment</Tabs.Trigger>
        </Tabs.List>
        <div className="mt-2">
          <Tabs.Content value="general">
            <Text size="small">This is the General tab.</Text>
          </Tabs.Content>
          <Tabs.Content value="shipping">
            <Text size="small">
              This is the Shipping tab (should be disabled).
            </Text>
          </Tabs.Content>
          <Tabs.Content value="payment">
            <Text size="small">This is the Payment tab.</Text>
          </Tabs.Content>
        </div>
      </Tabs>
      <Text size="xsmall" className="text-ui-fg-muted">
        Use the left and right arrow keys to navigate between the tabs. You must
        focus on a tab first.
      </Text>
    </div>
  )
}

```

### Tabs with Icons

```tsx
import { Tabs, Text } from "@medusajs/ui"
import { TruckFast, CreditCard, InformationCircle } from "@medusajs/icons"

export default function TabsIcons() {
  return (
    <div className="w-full px-4 flex flex-col gap-4">
      <Tabs defaultValue="general">
        <Tabs.List>
          <Tabs.Trigger value="general">
            <InformationCircle className="mr-1.5 h-4 w-4" /> General
          </Tabs.Trigger>
          <Tabs.Trigger value="shipping">
            <TruckFast className="mr-1.5 h-4 w-4" /> Shipping
          </Tabs.Trigger>
          <Tabs.Trigger value="payment">
            <CreditCard className="mr-1.5 h-4 w-4" /> Payment
          </Tabs.Trigger>
        </Tabs.List>
        <div className="mt-2">
          <Tabs.Content value="general">
            <Text size="small">This is the General tab with an icon.</Text>
          </Tabs.Content>
          <Tabs.Content value="shipping">
            <Text size="small">This is the Shipping tab with an icon.</Text>
          </Tabs.Content>
          <Tabs.Content value="payment">
            <Text size="small">This is the Payment tab with an icon.</Text>
          </Tabs.Content>
        </div>
      </Tabs>
      <Text size="xsmall" className="text-ui-fg-muted">
        Use the left and right arrow keys to navigate between the tabs. You must
        focus on a tab first.
      </Text>
    </div>
  )
}

```

### Vertical Tabs

The `orientation` prop doesn't change the layout of the tabs, but it allows you to navigate between the tabs using the up and down arrow keys. You'll need to manually style the tabs vertically.

```tsx
import { Tabs, Text } from "@medusajs/ui"

export default function TabsVertical() {
  return (
    <div className="w-full px-4 flex flex-col gap-4">
      <Tabs defaultValue="general" orientation="vertical" className="flex">
        <Tabs.List className="flex-col min-w-[120px] border-r border-ui-border-base">
          <Tabs.Trigger value="general">General</Tabs.Trigger>
          <Tabs.Trigger value="shipping">Shipping</Tabs.Trigger>
          <Tabs.Trigger value="payment">Payment</Tabs.Trigger>
        </Tabs.List>
        <div className="ml-6 flex-1">
          <Tabs.Content value="general">
            <Text size="small">This is the General tab (vertical).</Text>
          </Tabs.Content>
          <Tabs.Content value="shipping">
            <Text size="small">This is the Shipping tab (vertical).</Text>
          </Tabs.Content>
          <Tabs.Content value="payment">
            <Text size="small">This is the Payment tab (vertical).</Text>
          </Tabs.Content>
        </div>
      </Tabs>
      <Text size="xsmall" className="text-ui-fg-muted">
        Use the up and down arrow keys to navigate between the tabs. You must
        focus on a tab first.
      </Text>
    </div>
  )
}

```


# Text

A component that displays text using the typography styles from Medusa's design system.

In this guide, you'll learn how to use the Text component.

```tsx
import { Text } from "@medusajs/ui"

export default function TextDemo() {
  return <Text>Text</Text>
}

```

## Usage

```tsx
import { Text } from "@medusajs/ui"
```

```tsx
<Text>Text</Text>
```

***

## API Reference

### Text Props

This component is based on the \`p\` element and supports all of its props

- asChild: (boolean) Whether to remove the wrapper \`button\` element and use the
  passed child element instead. Default: false
- as: (union) The wrapper element to use when \`asChild\` is disabled. Default: "p"
- size: (union) The text's size. Default: "base"
- weight: (union) The text's font weight. Default: "regular"
- family: (union) The text's font family. Default: "sans"
- leading: (union) The text's line height. Default: "normal"

***

## Examples

### Text Sizes

```tsx
import { Text } from "@medusajs/ui"

export default function TextSizes() {
  return (
    <div className="flex flex-col gap-y-2">
      <Text size="base">Base size</Text>
      <Text size="large">Large size</Text>
      <Text size="xlarge">XLarge size</Text>
    </div>
  )
}

```

### Text Weights

```tsx
import { Text } from "@medusajs/ui"

export default function TextWeights() {
  return (
    <div className="flex flex-col gap-y-2">
      <Text weight="regular">Regular weight</Text>
      <Text weight="plus">Plus weight</Text>
    </div>
  )
}

```

### Text Fonts

```tsx
import { Text } from "@medusajs/ui"

export default function TextFonts() {
  return (
    <div className="flex flex-col gap-y-2">
      <Text family="sans">Sans font</Text>
      <Text family="mono">Mono font</Text>
    </div>
  )
}

```

### Text Leading

```tsx
import { Text } from "@medusajs/ui"

export default function TextLeading() {
  return (
    <div className="flex flex-col gap-y-2">
      <Text leading="normal">Normal leading</Text>
      <Text leading="compact">Compact leading</Text>
    </div>
  )
}

```


# Textarea

A component that displays a textarea field using Medusa's design system.

In this guide, you'll learn how to use the Textarea component.

```tsx
import { Textarea } from "@medusajs/ui"

export default function TextAreaDemo() {
  return <Textarea placeholder="Product description ..." />
}

```

## Usage

```tsx
import { Textarea } from "@medusajs/ui"
```

```tsx
<Textarea />
```

***

## API Reference

### Textarea Props

This component is based on the \`textarea\` element and supports all of its props



***

## Examples

### Controlled Textarea

```tsx
import { useState } from "react"
import { Textarea } from "@medusajs/ui"

export default function TextareaControlled() {
  const [value, setValue] = useState("")
  return (
    <div className="flex flex-col gap-y-2">
      <Textarea
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="Write your feedback..."
        aria-label="Feedback"
      />
      <div className="text-ui-fg-muted txt-compact-small">
        {value.length} characters
      </div>
    </div>
  )
}

```

### Disabled Textarea

```tsx
import { Textarea } from "@medusajs/ui"

export default function TextareaDisabled() {
  return (
    <Textarea
      disabled
      placeholder="Disabled textarea"
      aria-label="Disabled textarea"
    />
  )
}

```


# Toaster and Toast Messages

A component and utility for displaying brief messages to users, typically used for notifications or alerts. Toast messages appear momentarily on top of the application UI.

You can display multiple toast messages at once, and they will be stacked neatly.

In this guide, you'll learn how to use the Toaster component.

```tsx
import { Button, Toaster, toast } from "@medusajs/ui"

export default function ToasterDemo() {
  return (
    <>
      <Toaster />
      <Button
        onClick={() =>
          toast.info("Info", {
            description: "The quick brown fox jumps over the lazy dog.",
          })
        }
      >
        Show
      </Button>
    </>
  )
}

```

## Usage

First, import the `toast` utility and `Toaster` component from `@medusajs/ui`:

```tsx
import { Toaster, toast } from "@medusajs/ui"
```

Then, add the `Toaster` component somewhere in your tree hierarchy. For example, in your main application layout:

```tsx highlights={[["6"]]}
export default function AppLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Toaster />
      </body>
    </html>
  )
}
```

Finally, use the `toast` utility in your components to display a toast message:

```tsx highlights={[["5", "info", "Display an informational message"]]}
export default function MyComponent() {
  return (
    <Button 
      onClick={() => 
        toast.info("Toast title", {
          description: "Toast body",
        })
      }
    >
      Trigger
    </Button>
  )
}
```

***

## API Reference

### Toast Utility Functions

The `toast` utility has the following functions to display different variants of toast messages:

- `info`: Display a toast message with an informational style.
- `error`: Display a toast message with an error style.
- `success`: Display a toast message with a success style.
- `warning`: Display a toast message with a warning style.
- `loading`: Display a toast message with a loading style.

Each of these functions accept two parameters:

1. A string indicating the title of the toast.
2. An object of [Toast component props](#toast-props).

### Toast Props

### Toast Props

This component is based on the \[Sonner]\(https://sonner.emilkowal.ski/toast) toast library.

- id: (union) Optional ID of the toast.
- description: (ReactReactNode) The toast's text.
- action: (signature) The toast's action buttons.

### Toaster Props

### Toaster Props

This component is based on the \[Toaster component of the Sonner library]\(https://sonner.emilkowal.ski/toaster).

- position: (union) The position of the created toasts. Default: "bottom-right"
- gap: (number) The gap between the toast components. Default: 12
- offset: (union) The space from the edges of the screen. Default: 24
- duration: (number) The time in milliseconds that a toast is shown before it's
  automatically dismissed.

  &#x20;Default: 4000

***

## Examples

### Toast Variants

The following example assumes you already have the `Toaster` component in [your application's tree](#usage).

```tsx
import {
  CheckCircle,
  ExclamationCircle,
  InformationCircle,
  Spinner,
  XCircle,
} from "@medusajs/icons"
import { Button, toast } from "@medusajs/ui"

export default function ToasterAllVariants() {
  return (
    <div className="flex flex-wrap gap-2">
      <Button
        variant="secondary"
        onClick={() =>
          toast.info("Info", {
            description: "This is an info toast.",
          })
        }
      >
        <InformationCircle /> Info
      </Button>
      <Button
        variant="secondary"
        onClick={() =>
          toast.success("Success", {
            description: "This is a success toast.",
          })
        }
      >
        <CheckCircle /> Success
      </Button>
      <Button
        variant="secondary"
        onClick={() =>
          toast.error("Error", {
            description: "This is an error toast.",
          })
        }
      >
        <XCircle /> Error
      </Button>
      <Button
        variant="secondary"
        onClick={() =>
          toast.warning("Warning", {
            description: "This is a warning toast.",
          })
        }
      >
        <ExclamationCircle /> Warning
      </Button>
      <Button
        variant="secondary"
        onClick={() =>
          toast.loading("Loading", {
            description: "This is a loading toast.",
          })
        }
      >
        <Spinner /> Loading
      </Button>
    </div>
  )
}

```

### Dismissable Toast

The following example assumes you already have the `Toaster` component in [your application's tree](#usage).

```tsx
import { Button, toast } from "@medusajs/ui"

export default function DismissableToaster() {
  return (
    <Button
      onClick={() =>
        toast.info("Info", {
          description: "The quick brown fox jumps over the lazy dog.",
          dismissable: true,
        })
      }
    >
      Show
    </Button>
  )
}

```

### Toast with Action

The following example assumes you already have the `Toaster` component in [your application's tree](#usage).

```tsx
import { Button, toast } from "@medusajs/ui"

export default function ToasterWithAction() {
  return (
    <Button
      onClick={() =>
        toast.success("Created Product", {
          description: "The product has been created.",
          action: {
            altText: "Undo product creation",
            onClick: () => {},
            label: "Undo",
          },
          duration: 10000,
        })
      }
    >
      Show
    </Button>
  )
}

```


# Tooltip

A component that displays a pop-up with additional information when hovering over or focusing on an element.

In this guide, you'll learn how to use the Tooltip component.

```tsx
import { InformationCircleSolid } from "@medusajs/icons"
import { Tooltip } from "@medusajs/ui"

export default function TooltipDemo() {
  return (
    <Tooltip content="The quick brown fox jumps over the lazy dog.">
      <InformationCircleSolid />
    </Tooltip>
  )
}

```

## Usage

```tsx
import { Tooltip } from "@medusajs/ui"
```

```tsx
<Tooltip content="Tooltip content">Trigger</Tooltip>
```

***

## API Reference

### Tooltip Props

This component is based on the \[Radix UI Tooltip]\(https://www.radix-ui.com/primitives/docs/components/tooltip) primitive.

- content: (ReactReactNode) The content to display in the tooltip.
- onClick: (ReactMouseEventHandler) A function that is triggered when the tooltip is clicked.
- side: (union) The side to position the tooltip.

  &#x20;Default: top
- maxWidth: (number) The maximum width of the tooltip. Default: 220
- sideOffset: (number) The distance in pixels between the tooltip and its trigger. Default: 8
- children: (undefined) The element to trigger the tooltip.
- open: (boolean) Whether the tooltip is currently open.
- defaultOpen: (boolean) Whether the tooltip is open by default.
- onOpenChange: (signature) A function that is called when the tooltip's open state changes.
- delayDuration: (number) The time in milliseconds to delay the tooltip's appearance.

***

## Usage Outside Medusa Admin with TooltipProvider

If you're using the `Tooltip` component in a project other than the Medusa Admin, make sure to include the `TooltipProvider` somewhere up in your component tree:

```tsx
<TooltipProvider>
  <Tooltip content="Tooltip content">Trigger</Tooltip>
</TooltipProvider>
```

### TooltipProvider Reference

### TooltipProvider Props

- delayDuration: (number) The duration from when the pointer enters the trigger until the tooltip gets opened. Default: 100
- skipDelayDuration: (number) How much time a user has to enter another trigger without incurring a delay again. Default: 300
- disableHoverableContent: (boolean) When \`true\`, trying to hover the content will result in the tooltip closing as the pointer leaves the trigger.

***

## Examples

### Changing Tooltip Side

```tsx
import { Tooltip } from "@medusajs/ui"
import {
  ArrowLongDown,
  ArrowLongLeft,
  ArrowLongRight,
  ArrowLongUp,
} from "@medusajs/icons"

export default function TooltipSides() {
  return (
    <div className="flex gap-8 items-center justify-center">
      <Tooltip content="Top" side="top">
        <ArrowLongUp />
      </Tooltip>
      <Tooltip content="Bottom" side="bottom">
        <ArrowLongDown />
      </Tooltip>
      <Tooltip content="Left" side="left">
        <ArrowLongLeft />
      </Tooltip>
      <Tooltip content="Right" side="right">
        <ArrowLongRight />
      </Tooltip>
    </div>
  )
}

```

### Set Tooltip Max Width

```tsx
import { Tooltip } from "@medusajs/ui"
import { InformationCircleSolid } from "@medusajs/icons"

export default function TooltipMaxWidth() {
  return (
    <Tooltip
      content="This is a very long tooltip message that demonstrates how you can use the maxWidth prop to control the width of the tooltip."
      maxWidth={320}
      className="text-center"
    >
      <InformationCircleSolid />
    </Tooltip>
  )
}

```


# Install Medusa UI for Medusa Admin Customizations

In this guide, you'll learn how to use Medusa UI for building Medusa Admin customizations.

## Use Medusa UI in Medusa Admin

The `@medusajs/ui` and `@medusajs/icons` packages are already installed as dependencies of the `@medusajs/admin-sdk` package in your Medusa project. They're installed by default in your Medusa plugins as well.

So, you can import the packages and use them in your Medusa Admin customizations without any additional installation steps.

For example, to use the UI and icon packages in a UI route:

```tsx title="src/admin/routes/custom/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { Container, Heading } from "@medusajs/ui"

const CustomPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">This is my custom route</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Custom Route",
  icon: ChatBubbleLeftRight,
})

export default CustomPage
```

In this example, you use the [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/ui/app/components/container/index.html.md) and [Heading](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/ui/app/components/heading/index.html.md) components in the UI route. You also use the `ChatBubbleLeftRight` icon from the [Icons package](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/ui/app/icons/overview/index.html.md) for the UI route's sidebar item.

***

## Related Resources

If you're building Medusa Admin customizations, check out the following documentation guides:

- [Admin Widgets](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md): Insert custom components into existing Medusa Admin pages.
- [Admin UI Routes](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md): Add new pages to the Medusa Admin.
- [Admin Components & Layouts](https://docs.medusajs.com/resources/admin-components/index.html.md): Use Medusa UI to implement common Medusa Admin components and layouts for a consistent design in your customizations.


# Install Medusa UI in Standalone Projects

In this guide, you'll learn how to install and use Medusa UI in a standalone project.

Medusa UI is a React UI library that, while intended for use within Medusa projects, can also be used in any React project.

The icons package is installed independently from Medusa UI. Learn how to install it in the [Icons](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/ui/app/icons/overview/index.html.md) guide.

***

## Medusa UI Compatibility

To use Medusa UI in your standalone project, you must have:

- React 18+ installed. Most React-based frameworks and libraries, such as Next.js and Vite, are compatible with this requirement.
- [Tailwind CSS](https://v3.tailwindcss.com/) installed. The components in Medusa UI are styled using Tailwind CSS, so you'll need to install it in your project as well.
  - Medusa UI was built with Tailwind CSS v3, but it may also support v4.

***

## Step 1: Install Medusa UI

In your standalone project, install the Medusa UI package with the following command:

```bash npm2yarn
npm install @medusajs/ui
```

***

## Step 2: Install UI Presets

Medusa UI customizes Tailwind CSS classes to implement its design system, so you must also install the Medusa UI preset package.

To install the Medusa UI preset, run the following command:

```bash npm2yarn
npm install @medusajs/ui-preset --save-dev
```

***

## Step 3: Configure Tailwind CSS

Next, you'll need to configure Tailwind CSS to use the Medusa UI preset and explicitly add the paths to the Medusa UI components as content files.

### Tailwind CSS v3 Configurations

In Tailwind CSS v3, which is the recommended version to use with Medusa UI, you need to add the following configurations to your `tailwind.config.js` or `tailwind.config.ts` file:

1. Add the Medusa UI preset to the `presets` array.
2. Ensure that the `content` field includes the path to the Medusa UI package.

```js title="tailwind.config.js" highlights={[["5"]]}
module.exports = {
  presets: [require("@medusajs/ui-preset")],
  content: [
    // ...
    "./node_modules/@medusajs/ui/dist/**/*.{js,jsx,ts,tsx}",
  ],
  // ...
}
```

If your project is in a monorepo, you'll need to resolve the path to the `@medusajs/ui` package from the monorepo root:

```tsx title="tailwind.config.js" highlights={monorepoHighlights}
const path = require("path")

const uiPath = path.resolve(
  require.resolve("@medusajs/ui"),
  "../..",
  "\*_/_.{js,jsx,ts,tsx}"
)

module.exports = {
  presets: [require("@medusajs/ui-preset")],
  content: [
    // ...
    uiPath,
  ],
  // ...
}

```

### Tailwind CSS v4 Configurations

Medusa UI isn't officially compatible with Tailwind CSS v4 yet, so use it with caution.

In your CSS file that imports Tailwind CSS, add the following `@import`, `@config`, and `@source` directives:

```css
@import "tailwindcss";
@source "../node_modules/@medusajs/ui";
@config "@medusajs/ui-preset";
```

This will explicitly include the Medusa UI preset and its components in your Tailwind CSS build and apply the preset styles to your project.

***

## Step 4: Use Medusa UI in Standalone Projects

You can now start building your application with Medusa UI.

For example, you can use the `Button` in your custom components:

```tsx
import { Button } from "@medusajs/ui"

export function ButtonDemo() {
  return <Button>Button</Button>
}
```

Refer to the documentation of each component to learn about its props and usage.

***

## Update UI Packages in Standalone Projects

Medusa's design system packages, including `@medusajs/ui` and `@medusajs/ui-preset`, are versioned independently from other `@medusajs/*` packages. However, they're still released as part of Medusa's releases.

So, to find the latest updates and breaking changes to any of these packages, refer to the [release notes in the Medusa GitHub repository](https://github.com/medusajs/medusa/releases).

To update these packages in your standalone project, update their version in your `package.json` file and re-install dependencies. For example:

```json title="package.json"
{
  "dependencies": {
    "@medusajs/ui": "4.0.0",
    "@medusajs/ui-preset": "4.0.0"
  }
}
```


# Page Not Found

The page you were looking for isn't available.

If you're looking for Medusa v1 documentation, it's been moved to [docs.medusajs.com/v1](https://docs.medusajs.com/v1/index.html.md).

If you think this is a mistake, please [report this issue on GitHub](https://github.com/medusajs/medusa/issues/new?assignees=\&labels=type%3A+docs\&template=docs.yml).

## Other Resources

- [Get Started Docs](https://docs.medusajs.com/docs/learn/index.html.md)
- [Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md)
- [Admin API reference](https://docs.medusajs.com/api/admin)
- [Store API reference](https://docs.medusajs.com/api/store)


# Medusa UI Documentation

Welcome to Medusa UI, a React implementation of the Medusa design system.

Medusa UI is a collection of components, hooks, utility functions, icons, and [Tailwind CSS](https://tailwindcss.com/) classes that can be used to build
a consistent user interface across the Medusa Admin and client applications.

## Figma Design System

The Medusa UI design system is also available on [Figma](https://www.figma.com/community/file/1278648465968635936/medusa-ui). You can explore the components, icons, and design tokens used in Medusa UI.

This is especially useful if you have the [Figma Dev MCP](https://help.figma.com/hc/en-us/articles/32132100833559-Guide-to-the-Dev-Mode-MCP-Server) set up, allowing you to easily copy designs from the Figma Medusa UI file to your projects.

***

## Packages

Medusa UI is split into multiple packages. Each package is published to npm
and can be installed separately.

- `@medusajs/ui` - React components, hooks, and utility functions used
  in Medusa UI.
- `@medusajs/ui-preset` - Tailwind CSS preset containing all the classes
  used in Medusa UI.
- `@medusajs/icons` - Icons used in Medusa UI.

Learn how to install and use these packages either for [Medusa Admin](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/ui/app/installation/medusa-admin-extension/index.html.md) customizations or a [standalone project](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/ui/app/installation/standalone-project/index.html.md).

***

## How Medusa UI is Built

At its core, Medusa UI is a styled and slightly opinionated implementation of [Radix Primitives](https://www.radix-ui.com/primitives).
Our team has also referenced the fantastic [shadcn/ui](https://ui.shadcn.com/) for inspiration in certain implementations.

Our team strongly believes in keeping the components simple and
composable, much like Medusa's foundation. This allows you to build whatever you need.

Our team has tried to avoid overloading
the component API and, instead, has leveraged the native HTML API, which gets implemented
and respected accordingly, and passed to the underlying elements.


# clx

`clx` is a utility function that adds class names to your components, with support for conditional classes and merging Tailwind CSS classes.

In this guide, you'll learn how to use the `clx` utility function.

## Usage

The `clx` function is built using [clsx](https://www.npmjs.com/package/clsx) and [tw-merge](https://www.npmjs.com/package/tw-merge). It is intended to be used with [Tailwind CSS](https://tailwindcss.com/) to efficiently add classes to your components.

`clx` is useful for:

- Conditionally apply classes based on props or state. For example, you can apply the `hidden` class if a component's `open` state variable is `false`.
- Merge multiple strings into a single class name string. For example, you can apply class names to the component, and allow passing additional class names as props.
- Override conflicting Tailwind CSS classes. For example, if you specify a `p-2` class name on your component, and you pass a `p-4` class name as a prop, the `p-4` class will take precedence.
  - The last class name specified will take precedence over any previous class names.

For example:

```tsx
import { clx } from "@medusajs/ui"

type BoxProps = {
  className?: string
  children: React.ReactNode
  mt: "sm" | "md" | "lg"
}

const Box = ({ className, children, mt }: BoxProps) => {
  return (
    <div
      className={clx(
        "flex items-center justify-center",
        {
          "mt-4": mt === "sm",
          "mt-8": mt === "md",
          "mt-12": mt === "lg",
        },
        className
      )}
    >
      {children}
    </div>
  )
}
```

In the above example, you use `clx` to:

- Apply a base style.
- Apply a margin top that depends on the `mt` prop.
- Add class names passed as a prop.

`clx` ensures that Tailwind CSS classes are merged without style conflicts.

***

## API Reference

### clx Parameters

`clx` accepts any number of arguments, each of them can be of the following types:

- `string`: A string of class names to apply.

```tsx
clx("flex items-center justify-between")
```

- `Record<string, boolean>`: An object whose keys are the class names to apply, and the values are booleans indicating whether to apply the class names.

```tsx
clx({
  "flex items-center justify-between": isFlex,
})
```

- `Array`: An array of strings or objects to apply.

```tsx
clx([
  "flex items-center justify-between",
  {
    "hidden": isHidden,
  },
])
```


# B2B Recipe

This recipe provides the general steps to implement a B2B store with Medusa.

Medusa has a ready-to-use B2B starter that you install and use in [this GitHub repository](https://github.com/medusajs/b2b-starter-medusa).

## Overview

In a B2B store, you provide different types of customers with relevant pricing, products, shopping experience, and more.

Medusa’s Commerce Modules, including [Sales Channel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md), [Customer](../../commerce-modules/), and [Pricing](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/index.html.md) modules enable this setup out-of-the-box:

- **Sales Channel**: Use sales channels to set product availability per channel. In this case, create a B2B sales channel that includes only B2B products.
- **Customer**: Use customer groups to organize your customers into different groups. Then, you can apply different prices for each group.
- **Pricing**: Use price lists to set different prices for each B2B customer group, among other conditions.

In addition, Medusa’s extensible architecture and Framework for customization allow you to scope existing and custom features to specific customer groups or sales channels.

[Visionary: Frictionless B2B ecommerce with Medusa](https://medusajs.com/blog/visionary/)

***

## Create B2B Sales Channel

Sales channels allow you to set product availability per channel. For B2B use cases, you can create a B2B sales channel that includes only B2B products.

Then, on the storefront, you retrieve only the B2B products for B2B customers, which is explained more in the next section.

You can create a sales channel through the Medusa Admin or Admin REST APIs.

- [Using Medusa Admin](https://docs.medusajs.com/user-guide/settings/sales-channels/index.html.md): Create the sales channel using the Medusa Admin.
- [Using Admin API](https://docs.medusajs.com/api/admin#sales-channels_postsaleschannels): Create the sales channel using the REST APIs.

***

## Create a Publishable API Key

A publishable API key allows you to specify the context of client requests:

- You associate the publishable API key with one or more sales channels, such as the B2B sales channel.
- In a client such as a storefront, you pass the publishable API key in the header of your requests.

So, if you use the publishable API key associated with the B2B sales channel in your storefront, the Medusa server will only return products that are available in the B2B sales channel.

You can create a publishable API key through the Medusa Admin or the Admin REST APIs, then associate it with the B2B sales channel. Then, you can use this key when developing your B2B storefront.

### Create Publishable API Key

- [Using Medusa Admin](https://docs.medusajs.com/user-guide/settings/developer/publishable-api-keys/index.html.md): Create the API key using the Medusa Admin.
- [Using Admin API](https://docs.medusajs.com/api/admin#api-keys_postapikeys): Create the API key using the REST APIs.

### Associate Key with Sales Channel

- [Using Medusa Admin](https://docs.medusajs.com/user-guide/settings/developer/publishable-api-keys#manage-publishable-api-keys-sales-channels/index.html.md): Associate the key with the sales channel using the Medusa Admin.
- [Using Admin API](https://docs.medusajs.com/api/admin#api-keys_postapikeysidsaleschannels): Associate the key with the sales channel using the REST APIs.

***

## Add Products to B2B Sales Channel

You can manage products to be available in specific sales channels. For B2B, this allows you to add products that are only available to B2B customers.

You can create new products or add existing ones to the B2B sales channel using the Medusa Admin or Admin REST APIs.

### Create Products

- [Using Medusa Admin](https://docs.medusajs.com/user-guide/products/create/index.html.md): Create the products using the Medusa Admin.
- [Using Admin API](https://docs.medusajs.com/api/admin#products_postproducts): Create the products using the REST APIs.

### Add Products to Sales Channel

- [Using Medusa Admin](https://docs.medusajs.com/user-guide/settings/sales-channels#manage-products-in-sales-channel/index.html.md): Create the products using the Medusa Admin.
- [Using Admin API](https://docs.medusajs.com/api/admin#sales-channels_postsaleschannelsidproductsbatchadd): Add the products to the sales channel using the REST APIs.

***

## Add B2B Customers and Groups

Customer groups allow you to organize your customers into different groups. Then, you can apply different prices for each group.

This is useful for B2B sales, as you often negotiate special prices with each customer or company.

You can create a customer group for each B2B company, then add customers of that company to the group.

### Create Customers

- [Using Medusa Admin](https://docs.medusajs.com/user-guide/customers/manage/index.html.md): Create customers using the Medusa Admin.
- [Using Admin API](https://docs.medusajs.com/api/admin#customers_postcustomers): Create customers using the REST APIs.

### Assign Customers to Groups

- [Using Medusa Admin](https://docs.medusajs.com/user-guide/customers/manage#manage-customers-groups/index.html.md): Assign customer to groups using the Medusa Admin.
- [Using Admin API](https://docs.medusajs.com/api/admin#customer-groups_postcustomergroupsidcustomersbatch): Assign customer to groups using the REST APIs.

### Flexible Customizations: Create Custom Module

B2B use cases often require more complex customer management, such as managing roles in a company with employees having different privileges.

For more complex use cases, you can create a custom module that introduces data models like `Company`, `Employee`, and other relevant models.

Then, you can link those companies to existing customers and groups, allowing you to benefit from existing features like price lists for specific customer groups.

- [Create Module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md): Learn how to create a module.
- [Define Module Links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md): Define links between data models.

***

## Create B2B Price List

Price lists allow you to set different prices for each customer group, among other conditions. They're useful to override prices for custom use cases.

For B2B use cases, you can use price lists to set different prices for each B2B customer group. Then, B2B customers can see different prices on the storefront based on their group.

You can create a price list using the Medusa Admin or the Admin REST APIs. Make sure to set the B2B customer group(s) as a condition.

- [Using Medusa Admin](https://docs.medusajs.com/user-guide/price-lists/create/index.html.md): Create price list using the Medusa Admin.
- [Using Admin API](https://docs.medusajs.com/api/admin#price-lists_postpricelists): Create price list using the REST APIs.

***

## Customize Medusa Admin

Based on your use case, you may need to customize the Medusa Admin to add new widgets or pages.

For example, you may want to add a page to manage companies and their employees, or you may want to add a widget to show the company associated with a customer group.

The Medusa Admin is an extensible application within your Medusa application. You can customize it by:

- **Widgets**: Adding widgets to existing pages, such as the customer group page.
- **UI Routes**: Adding new pages to the Medusa Admin, such as a page to manage companies and employees.
- **Settings Pages**: Adding new pages to the Medusa Admin settings, such as a page to manage company settings.

- [Create Admin Widget](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md): Add widgets into existing admin pages.
- [Create Admin UI Routes](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md): Add new pages to your Medusa Admin.

[Create Admin Setting Page](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes#create-settings-page/index.html.md): Add new page to the Medusa Admin settings.

***

## Customize or Build Storefront

Medusa provides a Next.js Starter Storefront to use with your application. You can customize it for your B2B use case, such as adding a login page for B2B customers or expanding the profile page to show the company associated with the customer.

Alternatively, you can build your own storefront using the Medusa APIs. This headless approach gives you the flexibility to build a custom storefront without limitations on which tech stack you use, or the design of the storefront.

In your storefront, you can use the publishable API key you associated with your B2B sales channel to ensure only B2B products are retrieved.

- [Next.js Starter Storefront](https://docs.medusajs.com/nextjs-starter/index.html.md): Learn how to install and customize the Next.js Starter Storefront.
- [Storefront Development](https://docs.medusajs.com/storefront-development/index.html.md): Find guides to build your own storefront.

[Use Publishable API Keys](https://docs.medusajs.com/api/store#publishable-api-key): Learn how to use the publishable API key in client requests.


# Implement Bundled Products in Medusa

In this tutorial, you'll learn how to implement bundled products in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md), which are available out-of-the-box.

Medusa natively supports [inventory kits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md), which can be used to create bundled products. However, inventory kits don't support all features of bundled products, such as fulfilling the products in the bundle separately.

In this tutorial, you'll use Medusa's customizable Framework to implement bundled products. By building the bundled products feature, you can expand on it based on what's necessary for your use case.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

## Summary

By following this tutorial, you'll learn how to:

- Install and set up a Medusa application.
- Define models for bundled products.
- Link bundled products to Medusa's existing product model, allowing you to benefit from existing product features.
- Customize the add-to-cart flow to support bundled products.
- Customize the Next.js Starter Storefront to display bundled products.

![Bundled products system architecture diagram showing the relationship between bundled products and individual products and variants](https://res.cloudinary.com/dza7lstvk/image/upload/v1745855513/Medusa%20Resources/bundled-products-overview_r5zejm.jpg)

- [Bundled Products Repository](https://github.com/medusajs/examples/tree/main/bundled-products): Find the full code for this guide in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1746024108/OpenApi/Bundled_Products_vloupx.yaml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.

Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Bundled Product Module

In Medusa, you can build custom features in a [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

In the module, you define the data models necessary for a feature and the logic to manage these data models. Later, you can build commerce flows around your module.

In this step, you'll build a Bundled Product Module that defines the necessary data models to store and manage bundled products.

Refer to the [Modules documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to learn more.

### Create Module Directory

Modules are created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/bundled-product`.

### Create Data Models

A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

Refer to the [Data Models documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md) to learn more.

For the Bundled Product Module, you need to define two data models:

- `Bundle` for the bundle itself.
- `BundleItem` for the items in the bundle.

To create the `Bundle` data model, create the file `src/modules/bundled-product/models/bundle.ts` with the following content:

```ts title="src/modules/bundled-product/models/bundle.ts" highlights={bundleHighlights}
import { model } from "@medusajs/framework/utils"
import { BundleItem } from "./bundle-item"

export const Bundle = model.define("bundle", {
  id: model.id().primaryKey(),
  title: model.text(),
  items: model.hasMany(() => BundleItem, {
    mappedBy: "bundle",
  }),
})
```

You define the `Bundle` data model using the `model.define` method of the DML. It accepts the data model's table name as a first parameter, and the model's schema object as a second parameter.

The `Bundle` data model has the following properties:

- `id`: A unique ID for the bundle.
- `title`: The bundle's title.
- `items`: A one-to-many relation to the `BundleItem` data model, which you'll create next.

Learn more about defining data model properties in the [Property Types documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md).

To create the `BundleItem` data model, create the file `src/modules/bundled-product/models/bundle-item.ts` with the following content:

```ts title="src/modules/bundled-product/models/bundle-item.ts" highlights={bundleItemHighlights}
import { model } from "@medusajs/framework/utils"
import { Bundle } from "./bundle"

export const BundleItem = model.define("bundle_item", {
  id: model.id().primaryKey(),
  quantity: model.number().default(1),
  bundle: model.belongsTo(() => Bundle, {
    mappedBy: "items",
  }),
})
```

The `BundleItem` data model has the following properties:

- `id`: A unique ID for the bundle item.
- `quantity`: The quantity of the item in the bundle. It defaults to `1`.
- `bundle`: A many-to-one relation to the `Bundle` data model, which you defined earlier.

Learn more about defining data model relations in the [Relations documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/relationships/index.html.md).

### Create Module's Service

You now have the necessary data models in the Bundled Product Module, but you'll need to manage their records. You do this by creating a service in the module.

A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, allowing you to manage your data models, or connect to a third-party service, which is useful if you're integrating with external services.

Refer to the [Module Service documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md) to learn more.

To create the Bundled Product Module's service, create the file `src/modules/bundled-product/service.ts` with the following content:

```ts title="src/modules/bundled-product/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import { Bundle } from "./models/bundle"
import { BundleItem } from "./models/bundle-item"

export default class BundledProductModuleService extends MedusaService({
  Bundle,
  BundleItem,
}) {
}
```

The `BundledProductModuleService` extends `MedusaService` from the Modules SDK which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods.

So, the `BundledProductModuleService` class now has methods like `createBundles` and `retrieveBundleItem`.

Find all methods generated by the `MedusaService` in [the Service Factory reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md).

### Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/bundled-product/index.ts` with the following content:

```ts title="src/modules/bundled-product/index.ts"
import { Module } from "@medusajs/framework/utils"
import BundledProductsModuleService from "./service"

export const BUNDLED_PRODUCT_MODULE = "bundledProduct"

export default Module(BUNDLED_PRODUCT_MODULE, {
  service: BundledProductsModuleService,
})
```

You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `bundledProduct`. The name can only contain alphanumeric characters and underscores.
2. An object with a required property `service` indicating the module's service.

You also export the module's name as `BUNDLED_PRODUCT_MODULE` so you can reference it later.

### Add Module to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/bundled-product",
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

### Generate Migrations

Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module.

Refer to the [Migrations documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md) to learn more.

Medusa's CLI tool can generate the migrations for you. To generate a migration for the Bundled Product Module, run the following command in your Medusa application's directory:

```bash
npx medusa db:generate bundledProduct
```

The `db:generate` command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a `migrations` directory under `src/modules/bundled-product` that holds the generated migration.

Then, to reflect these migrations on the database, run the following command:

```bash
npx medusa db:migrate
```

The tables for the `Bundle` and `BundleItem` data models are now created in the database.

***

## Step 3: Link Bundles to Medusa Products

Medusa integrates modules into your application without implications or side effects by isolating modules from one another. This means you can't directly create relationships between data models in your module and data models in other modules.

Instead, Medusa provides the mechanism to define links between data models, and retrieve and manage linked records while maintaining module isolation. Links are useful to define associations between data models in different modules, or extend a model in another module to associate custom properties with it.

Refer to the [Module Isolation documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md) to learn more.

In this step, you'll define a link between:

- The `Bundle` data model in the Bundled Product Module and the `Product` data model in the Products Module. This link will allow you to benefit from existing product features, like prices, sales channels, and more.
- The `BundleItem` data model in the Bundled Product Module and the `Product` data model in the Products Module. This link will allow you to associate a bundle item with an existing product, where the customer chooses from their variants when purchasing the bundle.

Refer to the [Product Module's data models reference](https://docs.medusajs.com/references/product/models/index.html.md) to learn more about available data models in the Products Module.

### Bundle \<> Product Link

You can define links between data models in a TypeScript or JavaScript file under the `src/links` directory.

So, to define the link between a bundle and a product, create the file `src/links/bundle-product.ts` with the following content:

```ts title="src/links/bundle-product.ts"
import { defineLink } from "@medusajs/framework/utils"
import ProductModule from "@medusajs/medusa/product"
import BundledProductsModule from "../modules/bundled-product"

export default defineLink(
  BundledProductsModule.linkable.bundle,
  ProductModule.linkable.product
)
```

You define a link using the `defineLink` function from the Modules SDK. It accepts two parameters:

1. An object indicating the first data model part of the link. A module has a special `linkable` property that contains link configurations for its data models. So, you can pass the link configurations for the `Bundle` data model from the Bundled Product module.
2. An object indicating the second data model part of the link. You pass the linkable configurations of the Product Module's `Product` data model.

You'll later learn how to query and manage the linked records.

### BundleItem \<> Product Link

Next, you'll define the link between the `BundleItem` data model and the `Product` data model. Create the file `src/links/bundle-item-product.ts` with the following content:

```ts title="src/links/bundle-item-product.ts"
import { defineLink } from "@medusajs/framework/utils"
import ProductModule from "@medusajs/medusa/product"
import BundledProductsModule from "../modules/bundled-product"

export default defineLink(
  {
    linkable: BundledProductsModule.linkable.bundleItem,
    isList: true,
  },
  ProductModule.linkable.product
)
```

You define the link in the same way as the previous one, but you pass an object with a `isList` property set to `true` for the first parameter. This indicates that the link is a one-to-many relation, meaning that a product can be linked to multiple bundle items.

### Sync Links to Database

Medusa creates a table in the database for each link you define. So, you must run the migrations again to create the necessary tables:

```bash
npx medusa db:migrate
```

This will create tables for both links in the database. The tables will later store the IDs of the linked records.

Refer to the [Module Links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md) documentation to learn more about defining links and link tables.

***

## Step 4: Create Bundled Product Workflow

You're now ready to start implementing bundled-product features. The first one you'll implement is the ability to create a bundled product.

To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task. By using workflows, you can track their executions' progress, define roll-back logic, and configure other advanced features.

So, in this section, you'll learn how to create a workflow that creates a bundled product. Later, you'll execute this workflow in an API route.

Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

The workflow will have the following steps:

- [createBundleStep](#createBundleStep): Create a bundle
- [createBundleItemStep](#createBundleItemStep): Create the bundle items
- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md): Create the Medusa product associated with the bundle
- [createRemoteLinkStep](https://docs.medusajs.com/references/helper-steps/createRemoteLinkStep/index.html.md): Create the link between the bundle and the Medusa product
- [createRemoteLinkStep](https://docs.medusajs.com/references/helper-steps/createRemoteLinkStep/index.html.md): Create the link between the bundle items and the Medusa products
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the created bundle and its items.

You only need to implement the first two steps, as Medusa provides the rest in its `@medusajs/medusa/core-flows` package.

### createBundleStep

The first step of the workflow creates a bundle using the Bundled Product Module's service.

To create the step, create the file `src/workflows/steps/create-bundle.ts` with the following content:

```ts title="src/workflows/steps/create-bundle.ts" highlights={createBundleStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import BundledProductModuleService from "../../modules/bundled-product/service"
import { BUNDLED_PRODUCT_MODULE } from "../../modules/bundled-product"

type CreateBundleStepInput = {
  title: string
}

export const createBundleStep = createStep(
  "create-bundle",
  async ({ title }: CreateBundleStepInput, { container }) => {
    const bundledProductModuleService: BundledProductModuleService =
      container.resolve(BUNDLED_PRODUCT_MODULE)

    const bundle = await bundledProductModuleService.createBundles({
      title,
    })

    return new StepResponse(bundle, bundle.id)
  },
  async (bundleId, { container }) => {
    if (!bundleId) {
      return
    }
    const bundledProductModuleService: BundledProductModuleService =
      container.resolve(BUNDLED_PRODUCT_MODULE)
      
    await bundledProductModuleService.deleteBundles(bundleId)
  }
)
```

You create a step with `createStep` from the Workflows SDK. It accepts two parameters:

1. The step's unique name, which is `create-bundle`.
2. An async function that receives two parameters:
   - The step's input, which is in this case an object with the bundle's properties.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.

In the step function, you resolve the Bundled Product Module's service from the Medusa container using its `resolve` method, passing it the module's name as a parameter.

Then, you create the bundle using the `createBundles` method. As you remember, the Bundled Product Module's service extends the `MedusaService` which generates data-management methods for you.

A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters:

1. The step's output, which is the bundle created.
2. Data to pass to the step's compensation function.

Learn more about creating a step in the [Workflow documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

#### Compensation Function

The compensation function undoes the actions performed in a step. Then, if an error occurs during the workflow's execution, the compensation functions of executed steps are called to roll back the changes. This mechanism ensures data consistency in your application, especially as you integrate external systems.

The compensation function accepts two parameters:

1. The data passed from the step in the second parameter of `StepResponse`, which in this case is the ID of the created bundle.
2. An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

In the compensation function, you resolve the Bundled Product Module's service from the Medusa container and call the `deleteBundles` method to delete the bundle created in the step.

Refer to the [Compensation Function documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/compensation-function/index.html.md) to learn more.

### createBundleItemStep

Next, you'll create the second step that creates the items in the bundle.

To create the step, create the file `src/workflows/steps/create-bundle-items.ts` with the following content:

```ts title="src/workflows/steps/create-bundle-items.ts" highlights={createBundleItemsStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { BUNDLED_PRODUCT_MODULE } from "../../modules/bundled-product"
import BundledProductModuleService from "../../modules/bundled-product/service"

type CreateBundleItemsStepInput = {
  bundle_id: string
  items: {
    quantity: number
  }[]
}

export const createBundleItemsStep = createStep(
  "create-bundle-items",
  async ({ bundle_id, items }: CreateBundleItemsStepInput, { container }) => {
    const bundledProductModuleService: BundledProductModuleService =
      container.resolve(BUNDLED_PRODUCT_MODULE)

    const bundleItems = await bundledProductModuleService.createBundleItems(
      items.map((item) => ({
        bundle_id,
        quantity: item.quantity,
      }))
    )

    return new StepResponse(bundleItems, bundleItems.map((item) => item.id))
  },
  async (itemIds, { container }) => {
    if (!itemIds?.length) {
      return
    }

    const bundledProductModuleService: BundledProductModuleService =
      container.resolve(BUNDLED_PRODUCT_MODULE)

    await bundledProductModuleService.deleteBundleItems(itemIds)
  }
)
```

This step accepts the bundle ID and an array of bundle items to create.

In the step, you resolve the Bundled Product Module's service to create the bundle items. Then, you return the created bundle items.

You also pass the IDs of the created bundle items to the compensation function. In the compensation function, you delete the bundle items created in the step.

### Create Workflow

Now that you have all the necessary steps, you can create the workflow.

To create the workflow, create the file `src/workflows/create-bundled-product.ts` with the following content:

```ts title="src/workflows/create-bundled-product.ts" highlights={createBundledProductWorkflowHighlights}
import { CreateProductWorkflowInputDTO } from "@medusajs/framework/types"
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { createBundleStep } from "./steps/create-bundle"
import { createBundleItemsStep } from "./steps/create-bundle-items"
import { createProductsWorkflow, createRemoteLinkStep, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { BUNDLED_PRODUCT_MODULE } from "../modules/bundled-product"
import { Modules } from "@medusajs/framework/utils"

export type CreateBundledProductWorkflowInput = {
  bundle: {
    title: string
    product: CreateProductWorkflowInputDTO
    items: {
      product_id: string
      quantity: number
    }[]
  }
}

export const createBundledProductWorkflow = createWorkflow(
  "create-bundled-product",
  ({ bundle: bundleData }: CreateBundledProductWorkflowInput) => {
    const bundle = createBundleStep({
      title: bundleData.title,
    })

    const bundleItems = createBundleItemsStep({
      bundle_id: bundle.id,
      items: bundleData.items,
    })
    
    const bundleProduct = createProductsWorkflow.runAsStep({
      input: {
        products: [bundleData.product],
      },
    })

    createRemoteLinkStep([{
      [BUNDLED_PRODUCT_MODULE]: {
        bundle_id: bundle.id,
      },
      [Modules.PRODUCT]: {
        product_id: bundleProduct[0].id,
      },
    }])

    const bundleProducttemLinks = transform({
      bundleData,
      bundleItems,
    }, (data) => {
      return data.bundleItems.map((item, index) => ({
        [BUNDLED_PRODUCT_MODULE]: {
          bundle_item_id: item.id,
        },
        [Modules.PRODUCT]: {
          product_id: data.bundleData.items[index].product_id,
        },
      }))
    })

    createRemoteLinkStep(bundleProducttemLinks).config({
      name: "create-bundle-product-items-links",
    })

    // retrieve bundled product with items
    const { data } = useQueryGraphStep({
      entity: "bundle",
      fields: ["*", "items.*"],
      filters: {
        id: bundle.id,
      },
    })

    return new WorkflowResponse(data[0])
  }
)
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is an object holding the details of the bundle to create.

In the workflow's constructor function, you:

1. Create the bundle using the `createBundleStep`.
2. Create the bundle items using the `createBundleItemsStep`.
3. Create the Medusa product associated with the bundle using the `createProductsWorkflow`.
4. Create a link between the bundle and the Medusa product using the `createRemoteLinkStep`.
   - To create a link, you pass an array of objects. The keys of each object are the module names, and the values are objects with the IDs of the records to link.
5. Use `transform` to prepare the data to link bundle items to products.
   - You must use the `transform` function whenever you want to manipulate data in a workflow, as Medusa creates an internal representation of the workflow when the application starts, not when the workflow is executed. Learn more in the [Transform Data documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md).
6. Create a link between the bundle items and the Medusa products using the `createRemoteLinkStep`.
7. Retrieve the bundle and its items using the `useQueryGraphStep`.
   - `useQueryGraphStep` uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which allows you to retrieve data across modules.

A workflow must return an instance of `WorkflowResponse`. The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is the created bundle.

You'll test out this API route in a later step when you customize the Medusa Admin dashboard.

***

## Step 5: Create Bundled Product API Route

Now that you have the logic to create a bundled product, you need to expose it so that frontend clients, such as the Medusa Admin, can use it. You do this by creating an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

An API Route is an endpoint that exposes commerce features to external applications and clients, such as admin dashboards or storefronts. You'll create an API route at the path `/admin/bundled-products` that executes the workflow from the previous step.

Refer to the [API Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) to learn more.

### Implement API Route

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

So, to create an API route at the path `/admin/bundled-products`, create the file `src/api/admin/bundled-products/route.ts` with the following content:

API routes starting with `/admin` are protected by default. So, only authenticated admin users can access them.

```ts title="src/api/admin/bundled-products/route.ts" highlights={bundledProductsRouteHighlights}
import { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { z } from "zod"
import { 
  AdminCreateProduct,
} from "@medusajs/medusa/api/admin/products/validators"
import { 
  createBundledProductWorkflow, 
  CreateBundledProductWorkflowInput,
} from "../../../workflows/create-bundled-product"

export const PostBundledProductsSchema = z.object({
  title: z.string(),
  product: AdminCreateProduct(),
  items: z.array(z.object({
    product_id: z.string(),
    quantity: z.number(),
  })),
})

type PostBundledProductsSchema = z.infer<typeof PostBundledProductsSchema>

export async function POST(
  req: AuthenticatedMedusaRequest<PostBundledProductsSchema>,
  res: MedusaResponse
) {
  const { 
    result: bundledProduct,
  } = await createBundledProductWorkflow(req.scope)
    .run({
      input: {
        bundle: req.validatedBody,
      } as CreateBundledProductWorkflowInput,
    })

  res.json({
    bundled_product: bundledProduct,
  })
}
```

You first define a validation schema with [Zod](https://zod.dev/). You'll use this schema in a bit to enforce validation on requests sent to this API route.

Since you export a `POST` route handler function, you expose a `POST` API route at `/admin/bundled-products`. The route handler function accepts two parameters:

1. A request object with details and context on the request, such as body parameters or authenticated customer details.
2. A response object to manipulate and send the response.

`AuthenticatedMedusaRequest` accepts the request body's type as a type argument.

In the route handler function, you execute the `createBundledProductWorkflow` by invoking it, passing it the Medusa container (which is available on the `scope` property of the request object), then calling its `run` method.

You pass the request body parameters as an input to the workflow.

Finally, you return the created bundle in the response.

### Add Validation Middleware

Now that you have the API route, you need to enforce validation on requests send to the route. You can do this with a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

A middleware is a function executed when a request is sent to an API Route. It's executed before the route handler.

Learn more in the [Middlewares documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

Middlewares are created in the `src/api/middlewares.ts` file. So create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" highlights={middlewaresHighlights}
import {
  defineMiddlewares, 
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { PostBundledProductsSchema } from "./admin/bundled-products/route"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/admin/bundled-products",
      methods: ["POST"],
      middlewares: [
        validateAndTransformBody(PostBundledProductsSchema),
      ],
    },
  ],
})
```

To export the middlewares, you use the `defineMiddlewares` function. It accepts an object having a `routes` property, whose value is an array of middleware route objects. Each middleware route object has the following properties:

- `method`: The HTTP methods the middleware applies to, which is in this case `POST`.
- `matcher`: The path of the route the middleware applies to.
- `middlewares`: An array of middleware functions to apply to the route.
  - You apply the `validateAndTransformBody` that validates that the request body parameters match the Zod schema passed as a parameter.

The create bundled product route is now ready for use. You'll use it in an upcoming step when you customize the Medusa Admin dashboard.

***

## Step 6: Retrieve Bundles API Route

Before you start customizing the Medusa Admin, you need an API route that retrieves all bundles. You'll use this API route to show the bundles in a table on the Medusa Admin dashboard.

To create the API route, add the following at the end of `src/api/admin/bundled-products/route.ts`:

```ts title="src/api/admin/bundled-products/route.ts" highlights={getBundledProductsRouteHighlights}
export async function GET(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  const query = req.scope.resolve("query")

  const { 
    data: bundledProducts, 
    metadata: { count, take, skip } = {}, 
  } = await query.graph({
    entity: "bundle",
    ...req.queryConfig,
  })

  res.json({
    bundled_products: bundledProducts,
    count: count || 0,
    limit: take || 15,
    offset: skip || 0,
  })
}
```

Since you export a `GET` route handler function, you expose a `GET` API route at `/admin/bundled-products`.

In the route handler, you resolve [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) from the Medusa container. Then, you call its `graph` method to retrieve the bundles.

Notice that you pass to `query.graph` the `req.queryConfig` object. This object contains default query configurations related to pagination and the fields to be retrieved. You'll learn how to set the query configurations in a bit.

Finally, you return the bundles in the response with pagination parameters.

### Add Query Configurations

In the API route, you use the Query configurations to determine the fields to retrieve and pagination parameters. These can be configured in a middleware, allowing you to set the default value, but also allowing clients to modify them.

To add the query configurations, add a new middleware object in `src/api/middlewares.ts`:

```ts title="src/api/middlewares.ts" highlights={getBundledProductsMiddlewareHighlights}
// other imports...
import { validateAndTransformQuery } from "@medusajs/framework/http"
import { createFindParams } from "@medusajs/medusa/api/utils/validators"

export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/bundled-products",
      methods: ["GET"],
      middlewares: [
        validateAndTransformQuery(createFindParams(), {
          defaults: [
            "id", 
            "title", 
            "product.*", 
            "items.*", 
            "items.product.*",
          ],
          isList: true,
          defaultLimit: 15,
        }),
      ],
    },
  ],
})
```

You apply the `validateAndTransformQuery` middleware on `GET` requests to `/admin/bundled-products`. It accepts the following parameters:

1. A Zod schema to validate query parameters. You use Medusa's `createFindParams` function, which creates a Zod schema containing the following query parameters:
   - `fields`: The fields to retrieve in a bundle.
   - `limit`: The maximum number of bundles to retrieve.
   - `offset`: The number of bundles to skip before retrieving the bundles.
   - `order`: The fields to sort the result by.
2. An object of Query configurations that you accessed in the API route handler using `req.queryConfig`. It accepts the following parameters:
   - `defaults`: The default fields and relations to retrieve. You retrieve the bundle, its linked product, and its items with their linked products.
   - `isList`: Whether the API route returns a list of items.
   - `defaultLimit`: The default number of items to retrieve in a page.

Your API route is now ready for use. You'll test it out in the next step as you customize the Medusa Admin dashboard.

***

## Step 7: Add Bundles Page to Medusa Admin

Now that you have the necessary routes for admin users to manage and view bundled products, you'll customize the Medusa Admin to allow admin users to use these features.

You can add a new page to the Medusa Admin dashboard using a [UI route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md). A UI route is a React component that specifies the content to be shown in a new page in the Medusa Admin dashboard.

You'll create a UI route to display the list of bundled products in the Medusa Admin. Later, you'll add a form to create a bundled product.

Learn more in the [UI Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md).

### Initialize JS SDK

Medusa provides a [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) that you can use to send requests to the Medusa server from any client application, including your Medusa Admin customizations.

The JS SDK is installed by default in your Medusa application. To configure it, create the file `src/admin/lib/sdk.ts` with the following content:

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: "http://localhost:9000",
  debug: process.env.NODE_ENV === "development",
  auth: {
    type: "session",
  },
})
```

You create an instance of the JS SDK using the `Medusa` class from the JS SDK. You pass it an object having the following properties:

- `baseUrl`: The base URL of the Medusa server.
- `debug`: A boolean indicating whether to log debug information into the console.
- `auth`: An object specifying the authentication type. When using the JS SDK for admin customizations, you use the `session` authentication type.

### Create UI Route

UI routes are created under the `src/admin/routes` directory in a `page.tsx` file. The file's path, relative to `src/admin/routes`, is used as the page's path in the Medusa Admin dashboard.

So, to create a new page that shows the list of bundled products, create the file `src/admin/routes/bundled-products/page.tsx` with the following content:

```tsx title="src/admin/routes/bundled-products/page.tsx" highlights={bundledProductsPageHighlights}
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { CubeSolid } from "@medusajs/icons"

const BundledProductsPage = () => {
  // TODO add implementation
}

export const config = defineRouteConfig({
  label: "Bundled Products",
  icon: CubeSolid,
})

export default BundledProductsPage
```

In a UI route's file, you must export:

1. A React component that defines the page's content. You'll add the content in a bit.
2. A configuration object that indicates the title and icon used in the sidebar for the page.

Next, you'll use the [DataTable](https://docs.medusajs.com/ui/components/data-table/index.html.md) component from Medusa UI to show the list of bundled products in a table.

Add the following before the `BundledProductsPage` component:

```tsx title="src/admin/routes/bundled-products/page.tsx" highlights={bundledProductsPageHighlights2}
import { 
  Container,
  Heading,
  DataTable,
  useDataTable,
  createDataTableColumnHelper,
  DataTablePaginationState,
} from "@medusajs/ui"
import { useQuery } from "@tanstack/react-query"
import { useMemo, useState } from "react"
import { sdk } from "../../lib/sdk"
import { Link } from "react-router-dom"

type BundledProduct = {
  id: string
  title: string
  product: {
    id: string
  }
  items: {
    id: string
    product: {
      id: string
      title: string
    }
    quantity: number
  }[]
  created_at: Date
  updated_at: Date
}

const columnHelper = createDataTableColumnHelper<BundledProduct>()

const columns = [
  columnHelper.accessor("id", {
    header: "ID",
  }),
  columnHelper.accessor("title", {
    header: "Title",
  }),
  columnHelper.accessor("items", {
    header: "Items",
    cell: ({ row }) => {
      return row.original.items.map((item) => (
        <div key={item.id}>
          <Link to={`/products/${item.product.id}`}>
            {item.product.title}
          </Link>{" "}
          x {item.quantity}
        </div>
      ))
    },
  }),
  columnHelper.accessor("product", {
    header: "Product",
    cell: ({ row }) => {
      return (
        <Link to={`/products/${row.original.product?.id}`}>
          View Product
        </Link>
      )
    },
  }),
]

const limit = 15
```

You define the table's columns using `createDataTableColumnHelper` from Medusa UI. The table has the following columns:

- `ID`: The ID of the bundle.
- `Title`: The title of the bundle.
- `Items`: The items in the bundle. You show the title and quantity of each associated product with a link to its page.
- `Product`: A link to the Medusa product associated with the bundle.

You also define a `limit` constant that indicates the maximum number of bundles to retrieve in a page.

Learn more about the `createDataTableColumnHelper` function in the [DataTable documentation](https://docs.medusajs.com/ui/components/data-table#columns-preparation/index.html.md).

Next, replace the `BundledProductsPage` with the following implementation:

```tsx title="src/admin/routes/bundled-products/page.tsx" highlights={bundledProductsPageHighlights3}
const BundledProductsPage = () => {
  const [pagination, setPagination] = useState<DataTablePaginationState>({
    pageSize: limit,
    pageIndex: 0,
  })

  const offset = useMemo(() => {
    return pagination.pageIndex * limit
  }, [pagination])

  const { data, isLoading } = useQuery<{
    bundled_products: BundledProduct[]
    count: number
  }>({
    queryKey: ["bundled-products", offset, limit],
    queryFn: () => sdk.client.fetch("/admin/bundled-products", {
      method: "GET",
      query: {
        limit,
        offset,
      },
    }),
  })

  const table = useDataTable({
    columns,
    data: data?.bundled_products ?? [],
    isLoading,
    pagination: {
      state: pagination,
      onPaginationChange: setPagination,
    },
    rowCount: data?.count ?? 0,
  })

  return (
    <Container className="divide-y p-0">
      <DataTable instance={table}>
        <DataTable.Toolbar 
          className="flex items-start justify-between gap-2 md:flex-row md:items-center"
        >
          <Heading>Bundled Products</Heading>
        </DataTable.Toolbar>
        <DataTable.Table />
        <DataTable.Pagination />
      </DataTable>
    </Container>
  )
}
```

In the component, you define a state variable `pagination` to manage the pagination state of the table, and a memoized variable `offset` to calculate the number of items to skip before retrieving the bundles based on the current page.

Then, you use the `useQuery` hook from [Tanstack (React) Query](https://tanstack.com/query/latest) to retrieve the bundles from the API route. Tanstack Query is a data-fetching library with features like caching, pagination, and background updates.

In the query function of `useQuery`, you use the JS SDK to send a `GET` request to `/admin/bundled-products` of the Medusa server. You pass the `limit` and `offset` query parameters to support paginating the bundles.

Next, you initialize a table instance using the `useDataTable` hook from Medusa UI. Finally, you render the table in the page.

### Test it Out

To test out the UI route, start the Medusa application by running the following command:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard in your browser at `http://localhost:9000/app` and log in.

After you log in, you'll see a new "Bundled Products" item in the sidebar. Click on it to open the Bundled Products page.

The table will be empty as you haven't added any bundled products yet. You'll add the form to create a bundled product next.

![Bundled Product page with empty table](https://res.cloudinary.com/dza7lstvk/image/upload/v1745919655/Medusa%20Resources/Screenshot_2025-04-29_at_12.30.30_PM_nvsezf.png)

***

## Step 8: Create Bundled Product Form

In this step, you'll add a form that allows admin users to create a bundled product. The form will be shown in a modal when the user clicks on a "Create" button in the Bundled Products page.

The form will have the following fields:

- The title of the bundle.
- For each bundle item, a selector to choose the associated product, and a quantity input field.

### Create Form Component

To create the component that shows the form, create the file `src/admin/components/create-bundled-product.tsx` with the following content:

```tsx title="src/admin/components/create-bundled-product.tsx" highlights={createBundledProductComponentHighlights} collapsibleLines="1-13" expandButtonLabel="Show Imports"
import { 
  Button,
  FocusModal,
  Heading,
  Input,
  Label,
  Select,
  toast,
} from "@medusajs/ui"
import { useState, useRef, useCallback, useMemo } from "react"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"
import { HttpTypes } from "@medusajs/framework/types"

const CreateBundledProduct = () => {
  const [open, setOpen] = useState(false)
  const [title, setTitle] = useState("")
  const [items, setItems] = useState<{
    product_id: string | undefined
    quantity: number
  }[]>([
    {
      product_id: undefined,
      quantity: 1,
    },
  ])
  // TODO fetch products
}
export default CreateBundledProduct
```

You create a `CreateBundledProduct` component that defines the following state variables:

- `open`: A boolean indicating whether the modal is open or closed.
- `title`: The title of the bundle.
- `items`: An array of objects representing the items in the bundle. Each object has the following properties:
  - `product`: The ID of the product.
  - `quantity`: The quantity of the product in the bundle.

### Fetch Products in Form Component

Next, you need to retrieve the list of products in Medusa to show them in a selector input. Replace the `TODO` in the `CreateBundledProduct` with the following:

```tsx title="src/admin/components/create-bundled-product.tsx" highlights={createBundledProductComponentHighlights2}
const [products, setProducts] = useState<HttpTypes.AdminProduct[]>([])
const productsLimit = 15
const [currentProductPage, setCurrentProductPage] = useState(0)
const [productsCount, setProductsCount] = useState(0)
const hasNextPage = useMemo(() => {
  return productsCount ? productsCount > productsLimit : true
}, 
[productsCount, productsLimit])
const queryClient = useQueryClient()
useQuery({
  queryKey: ["products"],
  queryFn: async () => {
    const { products, count } = await sdk.admin.product.list({
      limit: productsLimit,
      offset: currentProductPage * productsLimit,
    })
    setProductsCount(count)
    setProducts((prev) => [...prev, ...products])
    return products
  },
  enabled: hasNextPage,
})

const fetchMoreProducts = () => {
  if (!hasNextPage) {
    return
  }
  setCurrentProductPage(currentProductPage + 1)
}

// TODO add creation logic
```

You define new state variables to store the products, the current page of products, and the total number of products.

You also define a `hasNextPage` memoized variable to determine whether there are more products to load.

Then, you use the `useQuery` hook from Tanstack Query to retrieve the products from the Medusa server. You call the `sdk.admin.product.list` method to retrieve the products, passing it the `limit` and `offset` query parameters.

Lastly, you define a `fetchMoreProducts` function that increments the current page of products, which triggers retrieving more products. You'll call this function whenever the user scrolls to the end of the products list.

### Add Creation Logic to Form Component

Next, you'll define the logic to create the bundled product in the Medusa server once the user submits the form.

Replace the new `TODO` with the following:

```tsx title="src/admin/components/create-bundled-product.tsx" highlights={createBundledProductComponentHighlights3}
const { 
  mutateAsync: createBundledProduct, 
  isPending: isCreating,
} = useMutation({
  mutationFn: async (data: Record<string, any>) => {
    await sdk.client.fetch("/admin/bundled-products", {
      method: "POST",
      body: data,
    })
  },
})

const handleCreate = async () => {
  try {
    await createBundledProduct({
      title,
      product: {
        title,
        options: [
          {
            title: "Default",
            values: ["default"],
          },
        ],
        status: "published",
        variants: [
          {
            title,
            // You can set prices in the product's page
            prices: [],
            options: {
              Default: "default",
            },
            manage_inventory: false,
          },
        ],
      },
      items: items.map((item) => ({
        product_id: item.product_id,
        quantity: item.quantity,
      })),
    })
    setOpen(false)
    toast.success("Bundled product created successfully")
    queryClient.invalidateQueries({
      queryKey: ["bundled-products"],
    })
    setTitle("")
    setItems([{ product_id: undefined, quantity: 1 }])
  } catch (error) {
    toast.error("Failed to create bundled product")
  }
}
```

You first define a mutation using the `useMutation` hook from Tanstack Query. The mutation is used to create the bundled product by sending a `POST` request to the `/admin/bundled-products` API route.

Then, you define a `handleCreate` function that will be called when the user submits the form. In this function, you:

- Create the bundled product using the `createBundledProduct` mutation. You pass it the details of the bundle, its product, and its items.
  - Notice that you don't set the prices. You can use custom logic to set the prices, or [set the price](https://docs.medusajs.com/user-guide/products/variants#edit-product-variant-prices/index.html.md) from the bundle's associated product page.
- Close the modal and show a success message using the `toast` component from Medusa UI.

### Add Component for Each Item in the Form

Before adding the UI for the form, you'll add a component that renders the form fields for each item in the bundle. You'll later render this as part of the form UI.

In the same file, add the following after the `CreateBundledProduct` component:

```tsx title="src/admin/components/create-bundled-product.tsx" highlights={createBundledProductComponentHighlights4}
type BundledProductItemProps = {
  item: { 
    product_id: string | undefined, 
    quantity: number, 
  }
  index: number
  setItems: React.Dispatch<React.SetStateAction<{
    product_id: string | undefined;
    quantity: number;
  }[]>>
  products: HttpTypes.AdminProduct[] | undefined
  fetchMoreProducts: () => void
  hasNextPage: boolean
}

const BundledProductItem = ({ 
  item, 
  index, 
  setItems, 
  products, 
  fetchMoreProducts, 
  hasNextPage,
}: BundledProductItemProps) => {
  const observer = useRef(
    new IntersectionObserver(
      (entries) => {
        if (!hasNextPage) {
          return
        }
        const first = entries[0]
        if (first.isIntersecting) {
          fetchMoreProducts()
        }
      },
      { threshold: 1 }
    )
  )

  const lastOptionRef = useCallback(
    (node: HTMLDivElement) => {
      if (!hasNextPage) {
        return
      }
      if (observer.current) {
        observer.current.disconnect()
      }
      if (node) {
        observer.current.observe(node)
      }
    },
    [hasNextPage]
  )

  return (
    <div className="my-2">
      <Heading level={"h3"} className="mb-2">Item {index + 1}</Heading>
        <Select 
          value={item.product_id} 
          onValueChange={(value) => 
            setItems((items) => 
              items.map((item, i) => {
                return i === index 
                  ? { 
                      ...item, 
                      product_id: value, 
                    } 
                  : item
              })
            )
          }
        >
          <Select.Trigger>
            <Select.Value placeholder="Select Product" />
          </Select.Trigger>
          <Select.Content>
            {products?.map((product, productIndex) => (
              <Select.Item 
                key={product.id} 
                value={product.id} 
                ref={
                  productIndex === products.length - 1 
                    ? lastOptionRef 
                    : null
                }
              >
                {product.title}
              </Select.Item>
            ))}
          </Select.Content>
        </Select>
        <div className="flex items-center gap-x-2 [&_div]:flex-1">
          <Label>Quantity</Label>
          <Input
            type="number"
            placeholder="Quantity"
            className="w-full mt-1 rounded-md border border-gray-200 p-2"
            value={item.quantity}
            onChange={(e) => 
              setItems((items) => 
                items.map((item, i) => {
                  return i === index 
                    ? { ...item, quantity: parseInt(e.target.value) } 
                    : item
                })
              )
            }
          />
        </div>
    </div>
  )
}
```

You define a `BundledProductItem` component that accepts the following props:

- `item`: The item in the bundle as stored in the `items` state variable.
- `index`: The index of the item in the `items` state variable.
- `setItems`: The state setter function to update the `items` state variable.
- `products`: The list of products retrieved from the Medusa server.
- `fetchMoreProducts`: The function to fetch more products when the user scrolls to the end of the list.
- `hasNextPage`: A boolean indicating whether there are more products to load.

In the component, you render the selector field using the [Select](https://docs.medusajs.com/ui/components/select/index.html.md) component from Medusa UI. You show the products as options in the select, and update the product ID in the `items` state variable whenever the user selects a product.

You also observe the last option in the list of products using the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). This allows you to fetch more products when the user scrolls to the end of the list.

Finally, you render an input field for the quantity of the item in the bundle. You update the quantity in the `items` state variable whenever the user changes it.

### Add Form UI

Now that you have the component to render each item in the bundle, you can add the form UI in the `CreateBundledProduct` component.

In `CreateBundledProduct`, add the following `return` statement

```tsx title="src/admin/components/create-bundled-product.tsx"
return (
  <FocusModal open={open} onOpenChange={setOpen}>
    <FocusModal.Trigger asChild>
      <Button variant="primary">Create</Button>
    </FocusModal.Trigger>
    <FocusModal.Content>
      <FocusModal.Header>
        <div className="flex items-center justify-end gap-x-2">
          <Heading level={"h1"}>Create Bundled Product</Heading>
        </div>
      </FocusModal.Header>
      <FocusModal.Body>
        <div className="flex flex-1 flex-col items-center overflow-y-auto">
          <div className="mx-auto flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
            <div>
              <Label>Bundle Title</Label>
              <Input
                value={title}
                onChange={(e) => setTitle(e.target.value)}
              />
            </div>
            <div>
              <Heading level={"h2"}>Bundle Items</Heading>
              {items.map((item, index) => (
                <BundledProductItem
                  key={index}
                  item={item}
                  index={index}
                  setItems={setItems}
                  products={products}
                  fetchMoreProducts={fetchMoreProducts}
                  hasNextPage={hasNextPage}
                />
              ))}
              <Button
                variant="secondary"
                onClick={() =>
                  setItems([
                    ...items,
                    { product_id: undefined, quantity: 1 },
                  ])
                }
              >
                Add Item
              </Button>
            </div>
          </div>
        </div>
      </FocusModal.Body>
      <FocusModal.Footer>
        <div className="flex items-center justify-end gap-x-2">
          <Button variant="secondary" onClick={() => setOpen(false)}>
            Cancel
          </Button>
          <Button
            variant="primary"
            onClick={handleCreate}
            isLoading={isCreating}
          >
            Create Bundle
          </Button>
        </div>
      </FocusModal.Footer>
    </FocusModal.Content>
  </FocusModal>
)
```

You use the [FocusModal](https://docs.medusajs.com/ui/components/focus-modal/index.html.md) component from Medusa UI to show the form in a modal. The modal is opened when the "Create" button is clicked.

In the modal, you show an input field for the bundle title, and you show the list of bundle items using the `BundledProductItem` component. You also add a button to add new items to the bundle.

Finally, you show a "Create Bundle" button that calls the `handleCreate` function when clicked to create the bundle.

### Add Form to Bundled Products Page

Now that the form component is ready, you'll add it to the Bundled Products page. This will show the button to open the modal with the form.

In `src/admin/routes/bundled-products/page.tsx`, add the following import at the top of the file:

```tsx title="src/admin/routes/bundled-products/page.tsx"
import CreateBundledProduct from "../../components/create-bundled-product"
```

Then, in the `DataTable.Toolbar` component, add the `CreateBundledProduct` component after the heading:

```tsx title="src/admin/routes/bundled-products/page.tsx" highlights={[["3"]]}
<DataTable.Toolbar className="flex items-start justify-between gap-2 md:flex-row md:items-center">
  <Heading>Bundled Products</Heading>
  <CreateBundledProduct />
</DataTable.Toolbar>
```

This will show the button to open the form at the right side of the page's header.

### Test it Out

To test out the form, start the Medusa application by running the following command:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard in your browser at `http://localhost:9000/app`, log in, and open the Bundled Products page.

Before creating the bundle, you may want to create the products in that bundle first. For example, if you're creating a "Camera Bundle", create "Camera" and "Camera Bag" products first.

You'll see a new "Create" button at the top right. Click on it to open the modal with the form.

![Create button shown at the top right of the Bundled Products page](https://res.cloudinary.com/dza7lstvk/image/upload/v1745922803/Medusa%20Resources/Screenshot_2025-04-29_at_1.32.36_PM_yoo23i.png)

In the modal:

- Enter a title for the bundle. This title will also be used to create the associated product.
- For each item:
  - Select a product from the dropdown. You can scroll to the end of the list to load more products.
  - Enter a quantity for the item.
  - To add a new item, click on the "Add Item" button.
- Once you're done, click on the "Create Bundle" button to create the bundle.

![Create bundled product form](https://res.cloudinary.com/dza7lstvk/image/upload/v1745923393/Medusa%20Resources/Screenshot_2025-04-29_at_1.42.12_PM_mdyzsi.png)

After you create the bundle, the modal will close, and you can see the bundle in the table.

### Edit Associated Product

Once you have a bundle, you can go to its associated product page using the "View Product" link in the table.

In the associated product's page, you should:

- Set the sales channel that the product is available in to ensure it's available for sale.
- Set the shipping profile the product belongs to. This will allow customers to select the appropriate shipping option for the bundle during checkout.
- You can optionally edit other product details, such as the title, description, and images.

Learn more about editing a product in the [User Guide](https://docs.medusajs.com/user-guide/products/edit/index.html.md)

![Associated product page](https://res.cloudinary.com/dza7lstvk/image/upload/v1745923661/Medusa%20Resources/Screenshot_2025-04-29_at_1.46.52_PM_iuplxc.png)

***

## Step 9: Add Bundled Product to Cart

Now that you have bundled products, you need to support adding them to the cart.

In the storefront, when the customer adds the bundle to the cart, they'll select the variant for each item. For example, they can choose a "Black" or "Blue" camera bag.

So, you need to build a flow that adds the chosen product variants of the bundle's items to the cart. You'll add the variants with their default price and the quantity specified in the bundle.

You can customize this logic to fit your needs, such as adding the bundle as a single item in the cart with its total price, or setting custom price for each of the items.

To implement the add-to-cart logic for bundled products, you will:

- Create a workflow that implements the logic.
- Execute the workflow in an API route for storefronts.

### Create Workflow

The add-to-cart workflow for bundled products has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the details of a bundle, its items, and their products and variants.
- [prepareBundleCartDataStep](#prepareBundleCartDataStep): Validate and prepare the items to be added to the cart.
- [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md): Acquire a lock on the cart to prevent concurrent modifications
- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md): Add the items in the bundle to the cart.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the details of the cart.
- [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md): Release the lock on the cart.

You only need to implement the second step, as the other steps are provided by Medusa's `@medusajs/medusa/core-flows` package.

#### a. prepareBundleCartDataStep

The second step of the workflow validates that the customer chose valid variants for each bundle item, and returns the items to be added to the cart.

To create the step, create the file `src/workflows/steps/prepare-bundle-cart-data.ts` with the following content:

```ts title="src/workflows/steps/prepare-bundle-cart-data.ts" highlights={prepareBundleCartDataStepHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { InferTypeOf, ProductDTO } from "@medusajs/framework/types"
import { Bundle } from "../../modules/bundled-product/models/bundle"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { MedusaError } from "@medusajs/framework/utils"
import { BundleItem } from "../../modules/bundled-product/models/bundle-item"

type BundleItemWithProduct = InferTypeOf<typeof BundleItem> & {
  product: ProductDTO
}

export type PrepareBundleCartDataStepInput = {
  bundle: InferTypeOf<typeof Bundle> & {
    items: BundleItemWithProduct[]
  }
  quantity: number
  items: {
    item_id: string
    variant_id: string
  }[]
}

export const prepareBundleCartDataStep = createStep(
  "prepare-bundle-cart-data",
  async ({ bundle, quantity, items }: PrepareBundleCartDataStepInput) => {
    const bundleItems = bundle.items.map((item: BundleItemWithProduct) => {
      const selectedItem = items.find((i) => i.item_id === item.id)
      if (!selectedItem) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA, 
          `No variant selected for bundle item ${item.id}`
        )
      }
      const variant = item.product.variants.find((v) => 
        v.id === selectedItem.variant_id
      )
      if (!variant) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA, 
          `Variant ${
            selectedItem.variant_id
          } is invalid for bundle item ${item.id}`
        )
      }
      return {
        variant_id: selectedItem.variant_id,
        quantity: item.quantity * quantity,
        metadata: {
          bundle_id: bundle.id,
          quantity: quantity,
        },
      }
    })

    return new StepResponse(bundleItems)
  }  
)
```

The step receives as an input the bundle's details, the quantity of the bundle to add to the cart, and the selected variants for each item in the bundle.

In the step, you throw an error if an item in the bundle doesn't have a selected variant, or if the selected variant is invalid for that item.

Otherwise, you return an array of objects representing the items to be added to the cart. Each object has the following properties:

- `variant_id`: The ID of the selected variant to add to the cart.
- `quantity`: The quantity of the variant to add to the cart. This is calculated by multiplying the quantity of the item in the bundle with the quantity of the bundle to add to the cart.
- `metadata`: A line item in the cart has a `metadata` property that can be used to store custom key-value pairs. You store in it the ID of the bundle and its quantity that was added to the cart. This will be useful later when you want to retrieve the item's bundle.

#### Using Custom Prices

If you want to add the items to the cart with custom prices, you can modify the returned object in the loop to include a `unit_price` property. For example:

```ts highlights={[["4"]]}
return {
  variant_id: selectedItem.variant_id,
  quantity: item.quantity * quantity,
  unit_price: 100,
  metadata: {
    bundle_id: bundle.id,
    quantity: quantity,
  },
}
```

The item will then be added to the cart with that price. Note that the currency is based on the cart's currency.

For example, if the cart's currency is `usd`, then you're adding an item to the cart at the price `$100`.

#### b. Implement the Workflow

You can now create the workflow with the custom add-to-cart logic.

To create the workflow, create the file `src/workflows/add-bundle-to-cart.ts` with the following content:

```ts title="src/workflows/add-bundle-to-cart.ts" highlights={addBundleToCartWorkflowHighlights} collapsibleLines="1-16" expandButtonLabel="Show Imports"
import { 
  createWorkflow, 
  transform, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  acquireLockStep,
  addToCartWorkflow,
  releaseLockStep, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { 
  prepareBundleCartDataStep, 
  PrepareBundleCartDataStepInput,
} from "./steps/prepare-bundle-cart-data"

type AddBundleToCartWorkflowInput = {
  cart_id: string
  bundle_id: string
  quantity: number
  items: {
    item_id: string
    variant_id: string
  }[]
}

export const addBundleToCartWorkflow = createWorkflow(
  "add-bundle-to-cart",
  ({ cart_id, bundle_id, quantity, items }: AddBundleToCartWorkflowInput) => {
    const { data } = useQueryGraphStep({
      entity: "bundle",
      fields: [
        "id",
        "items.*",
        "items.product.*",
        "items.product.variants.*",
      ],
      filters: {
        id: bundle_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })
    
    const itemsToAdd = prepareBundleCartDataStep({
      bundle: data[0],
      quantity,
      items,
    } as unknown as PrepareBundleCartDataStepInput)

    acquireLockStep({
      key: cart_id,
      timeout: 2,
      ttl: 10,
    })

    addToCartWorkflow.runAsStep({
      input: {
        cart_id,
        items: itemsToAdd,
      },
    })

    const { data: updatedCarts } = useQueryGraphStep({
      entity: "cart",
      filters: { id: cart_id },
      fields: ["id", "items.*"],
    }).config({ name: "refetch-cart" })

    releaseLockStep({
      key: cart_id,
    })

    return new WorkflowResponse(updatedCarts[0])
  }
)
```

The workflow accepts as an input the cart's ID, the bundle's ID, the quantity of the bundle to add to the cart, and the selected variants for each item in the bundle.

In the workflow, you:

- Retrieve the bundle, its items, and their products and variants using the `useQueryGraphStep`.
- Validate and prepare the items to be added to the cart using the `prepareBundleCartDataStep`.
- Acquire a lock on the cart using the `acquireLockStep`.
- Add the items to the cart using the `addToCartWorkflow`.
- Retrieve the updated cart using the `useQueryGraphStep`.
- Release the lock on the cart using the `releaseLockStep`.

Finally, you return the updated cart.

### Create API Route

You'll now create the API route that exposes the workflow's functionalities to storefronts.

To create the API route, create the file `src/api/store/carts/[id]/line-item-bundles/route.ts` with the following content:

```ts title="src/api/store/carts/[id]/line-item-bundles/route.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { z } from "zod"
import { 
  addBundleToCartWorkflow,
} from "../../../../../workflows/add-bundle-to-cart"

export const PostCartsBundledLineItemsSchema = z.object({
  bundle_id: z.string(),
  quantity: z.number().default(1),
  items: z.array(z.object({
    item_id: z.string(),
    variant_id: z.string(),
  })),
})

type PostCartsBundledLineItemsSchema = z.infer<
  typeof PostCartsBundledLineItemsSchema
>

export async function POST(
  req: MedusaRequest<PostCartsBundledLineItemsSchema>,
  res: MedusaResponse
) {
  const { result: cart } = await addBundleToCartWorkflow(req.scope)
    .run({
      input: {
        cart_id: req.params.id,
        bundle_id: req.validatedBody.bundle_id,
        quantity: req.validatedBody.quantity || 1,
        items: req.validatedBody.items,
      },
    })

  res.json({
    cart,
  })
}
```

You first define a Zod schema to validate the request body. The schema has the following properties:

- `bundle_id`: The ID of the bundle to add to the cart.
- `quantity`: The quantity of the bundle to add to the cart. This is optional and defaults to `1`.
- `items`: An array of objects representing the selected variants for each item in the bundle. Each object has the following properties:
  - `item_id`: The ID of the item in the bundle.
  - `variant_id`: The ID of the selected variant for that item.

Then, you export a `POST` route handler, which exposes a `POST` API route at `/store/carts/:id/line-item-bundles`.

In the route handler, you execute the `addBundleToCartWorkflow` workflow. Finally, you return the cart's details in the response.

### Add Validation Middleware

Lastly, you need to add the middleware that enforces the validation of incoming request bodies.

In `src/api/middlewares.ts`, add a new middleware object to the `routes` array:

```ts title="src/api/middlewares.ts"
// other imports...
import { 
  PostCartsBundledLineItemsSchema,
} from "./store/carts/[id]/line-item-bundles/route"

export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/store/carts/:id/line-item-bundles",
      methods: ["POST"],
      middlewares: [
        validateAndTransformBody(PostCartsBundledLineItemsSchema),
      ],
    },
  ],
})
```

This middleware will validate the request body against the `PostCartsBundledLineItemsSchema` schema before executing the route handler.

You can now use the API route to add bundles to the cart. You'll test it out in the upcoming sections when you customize the Next.js Starter Storefront.

***

## Step 10: Retrieve Bundled Product API Route

Before customizing the storefront, you'll create an API route to retrieve the details of a bundled product. This will be useful to show the bundle's details in the storefront.

To create the API route, create the file `src/api/store/bundle-products/[id]/route.ts` with the following content:

```ts title="src/api/store/bundle-products/[id]/route.ts" highlights={bundleProductsRouteHighlights} 
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { QueryContext } from "@medusajs/framework/utils"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { id } = req.params
  const query = req.scope.resolve("query")
  const { currency_code, region_id } = req.query

  const { data } = await query.graph({
    entity: "bundle",
    fields: [
      "*", 
      "items.*", 
      "items.product.*", 
      "items.product.options.*",
      "items.product.options.values.*",
      "items.product.variants.*",
      "items.product.variants.calculated_price.*",
      "items.product.variants.options.*",
    ],
    filters: {
      id,
    },
    context: {
      items: {
        product: {
          variants: {
            calculated_price: QueryContext({
              region_id,
              currency_code,
            }),
          },
        },
      },
    },
  
  }, {
    throwIfKeyNotFound: true,
  })

  res.json({
    bundle_product: data[0],
  })
}
```

You export a `GET` route handler, which exposes a `GET` API route at `/store/bundle-products/:id`.

In the route handler, you resolve [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) from the Medusa container.

Then, you use Query to retrieve the bundle with its items and their products, variants, and options. These are useful to display to the customer the options for each product to select from, which will result in selecting a variant for a bundle item.

To retrieve the correct price for each variant, you also pass a [Query Context](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query-context/index.html.md) with the region ID and currency code that are passed as query parameters. This ensures that the prices are shown accurately to the customer.

Refer to the [Get Product Variant Prices](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price/index.html.md) guide to learn more about how to retrieve the prices of a product variant.

Finally, you return the bundle's details in the response.

You'll use this API route next as you customize the storefront.

***

## Step 11: Show Bundled Product Details in Storefront

In this step, you'll customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) you installed with the Medusa application to show a bundled product's items.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-bundled-products`, you can find the storefront by going back to the parent directory and changing to the `medusa-bundled-products-storefront` directory:

```bash
cd ../medusa-bundled-products-storefront # change based on your project name
```

### Add Function to Retrieve Bundled Product

You'll start by adding a server action function that retrieves the details of a bundled product.

In `src/lib/data/products.ts`, add the following at the end of the file:

```ts title="src/lib/data/products.ts" badgeLabel="Storefront" badgeColor="blue" highlights={getBundleProductHighlights}
export type BundleProduct = {
  id: string
  title: string
  product: {
    id: string
    thumbnail: string
    title: string
    handle: string
  }
  items: {
    id: string
    title: string
    product: HttpTypes.StoreProduct
  }[]
}

export const getBundleProduct = async (id: string, {
  currency_code,
  region_id,
}: {
  currency_code?: string
  region_id?: string
}) => {
  const headers = {
    ...(await getAuthHeaders()),
  }

  return sdk.client.fetch<{
    bundle_product: BundleProduct
  }>(`/store/bundle-products/${id}`, {
    method: "GET",
    headers,
    query: {
      currency_code,
      region_id,
    },
  })
}
```

You define a `BundledProduct` type that represents the structure of a bundled product.

Then, you define a `getBundleProduct` function that retrieves the bundle's details from the API route you created in the previous step.

### Retrieve Bundle with Product

Since a bundle is linked to a Medusa product, you can modify the request that retrieves the Medusa product to retrieve its associated bundle, if there's any.

By retrieving the bundle's details, you can check which Medusa product is a bundled product, then retrieve its full bundle details.

To retrieve a product's bundle details, first, change the signature of the `listProducts` function in `src/lib/data/products.ts` to the following:

```ts title="src/lib/data/products.ts" badgeLabel="Storefront" badgeColor="blue" highlights={listProductsHighlights}
export const listProducts = async ({
  pageParam = 1,
  queryParams,
  countryCode,
  regionId,
}: {
  pageParam?: number
  queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams
  countryCode?: string
  regionId?: string
}): Promise<{
  response: { products: (HttpTypes.StoreProduct & {
    bundle?: Omit<BundleProduct, "items">
  })[]; count: number }
  nextPage: number | null
  queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams
}> => {
  // ...
}
```

You modify the response type to possibly include the bundle details (without the `items`) in each product.

Next, find the `sdk.client.fetch` call in `listProducts` and replace the type argument of `fetch` with the following:

```ts title="src/lib/data/products.ts" badgeLabel="Storefront" badgeColor="blue" highlights={fetchProductsHighlight}
return sdk.client
  .fetch<{ products: (HttpTypes.StoreProduct & {
    bundle?: Omit<BundleProduct, "items">
  })[]; count: number }>(
    // ...
  )
```

This will ensure that the response from the API route is typed correctly.

Then, in `src/app/[countryCode]/(main)/products/[handle]/page.tsx`, add the following import at the top of the file:

```ts title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue"
import { getBundleProduct } from "@lib/data/products"
```

After that, in the `ProductPage` component in the same file, find the declaration of `pricedProduct` and update the query parameters passed to `listProducts`:

```ts title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"]]}
const pricedProduct = await listProducts({
  countryCode: params.countryCode,
  queryParams: { 
    handle: params.handle, 
    fields: "*bundle",
  },
}).then(({ response }) => response.products[0])
```

You add the `fields` query parameter an set it to `*bundle`. This will ensure that the bundle details are included in the retrieved product objects.

Next, after the `if` condition that checks if `pricedProduct` isn't `undefined`, add the following code:

```ts title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue"
const bundleProduct = pricedProduct.bundle ? 
    await getBundleProduct(pricedProduct.bundle.id, {
      currency_code: region.currency_code,
      region_id: region.id,
    }) : null
```

This will retrieve the full bundled product details if the product is associated with a bundle.

### Add Bundle to Cart Function

Next, you'll add a function that adds the bundle to the cart using the API route you created in the previous step.

In `src/lib/data/cart.ts`, add the following function at the end of the file:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
export async function addBundleToCart({
  bundleId,
  quantity,
  countryCode,
  items,
}: {
  bundleId: string
  quantity: number
  countryCode: string
  items: {
    item_id: string
    variant_id: string
  }[]
}) {
  if (!bundleId) {
    throw new Error("Missing bundle ID when adding to cart")
  }

  const cart = await getOrSetCart(countryCode)

  if (!cart) {
    throw new Error("Error retrieving or creating cart")
  }

  const headers = {
    ...(await getAuthHeaders()),
  }

  await sdk.client.fetch<HttpTypes.StoreCartResponse>(
    `/store/carts/${cart.id}/line-item-bundles`,
  {
    method: "POST",
    body: {
      bundle_id: bundleId,
      quantity,
      items,
    },
    headers,
  })
    .then(async () => {
      const cartCacheTag = await getCacheTag("carts")
      revalidateTag(cartCacheTag)

      const fulfillmentCacheTag = await getCacheTag("fulfillment")
      revalidateTag(fulfillmentCacheTag)
    })
    .catch(medusaError)
}
```

You define the `addBundleToCart` function that sends a `POST` request to the API route you created in the previous step.

The request body includes the bundle ID, quantity, and selected variants for each item in the bundle.

### Show Bundle Item Selection Actions

You'll now add a component that shows for bundled product their items and allow the customer to select the product variant for each item, then add it to the cart.

#### a. Add Bundle Actions Component

To create the component, create the file `src/modules/products/components/bundle-actions/index.tsx` with the following content:

```tsx title="src/modules/products/components/bundle-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={bundleActionsComponentHighlights} collapsibleLines="1-13" expandButtonLabel="Show Imports"
"use client"

import { addBundleToCart } from "@lib/data/cart"
import { HttpTypes } from "@medusajs/types"
import { Button } from "@medusajs/ui"
import OptionSelect from "@modules/products/components/product-actions/option-select"
import { BundleProduct } from "@lib/data/products"
import { isEqual } from "lodash"
import { useParams } from "next/navigation"
import { useEffect, useMemo, useState } from "react"
import ProductPrice from "../product-price"
import Thumbnail from "../thumbnail"

type BundleActionsProps = {
  bundle: BundleProduct
}

const optionsAsKeymap = (
  variantOptions: HttpTypes.StoreProductVariant["options"]
) => {
  return variantOptions?.reduce((acc: Record<string, string>, varopt: any) => {
    acc[varopt.option_id] = varopt.value
    return acc
  }, {})
}

export default function BundleActions({
  bundle,
}: BundleActionsProps) {
  const [productOptions, setProductOptions] = useState<
    Record<string, Record<string, string>>
  >({})
  const [isAdding, setIsAdding] = useState(false)
  const countryCode = useParams().countryCode as string

  // TODO retrieve and set selected variants and options
}
```

First, you define an `optionsAsKeymap` function that converts the product variant options into a key-value map. This is useful to later compare the selected options with the available options.

Then, you define the `BundleActions` component that accepts a `bundle` prop. In the component, you define:

- `productOptions`: A state variable that stores the selected options for each product in the bundle. The key is the product ID, and the value is a key-value map of the selected options.
- `isAdding`: A state variable that indicates whether the bundle is being added to the cart.
- `countryCode`: The country code from the URL parameters.

#### b. Selected Variants and Options Logic

Next, you'll add the logic to retrieve and set the selected variants and options for each product in the bundle.

In `BundleActions`, replace the `TODO` with the following:

```tsx title="src/modules/products/components/bundle-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={bundleActionsComponentHighlights2}
// For each product, if it has only 1 variant, preselect it
useEffect(() => {
  const initialOptions: Record<string, Record<string, string>> = {}
  bundle.items.forEach((item) => {
    if (item.product.variants?.length === 1) {
      const variantOptions = optionsAsKeymap(item.product.variants[0].options)
      initialOptions[item.product.id] = variantOptions ?? {}
    } else {
      initialOptions[item.product.id] = {}
    }
  })
  setProductOptions(initialOptions)
}, [bundle.items])

const selectedVariants = useMemo(() => {
  return bundle.items.map((item) => {
    if (!item.product.variants || item.product.variants.length === 0) {return undefined}

    return item.product.variants.find((v) => {
      const variantOptions = optionsAsKeymap(v.options)
      return isEqual(variantOptions, productOptions[item.product.id])
    })
  })
}, [bundle.items, productOptions])

const setOptionValue = (productId: string, optionId: string, value: string) => {
  setProductOptions((prev) => ({
    ...prev,
    [productId]: {
      ...prev[productId],
      [optionId]: value,
    },
  }))
}

const allVariantsSelected = useMemo(() => {
  return selectedVariants.every((v) => v !== undefined)
}, [selectedVariants])

// TODO handle add to cart
```

In the `useEffect` hook, you check if each product in the bundle has only one variant. If it does, you preselect that variant's options. This ensures the customer doesn't need to select the options if there's only one variant available.

Then, you define a `selectedVariants` variable that stores the selected variants for each product in the bundle. A selected variant is inferred if all options of a product are selected.

You also define a `setOptionValue` function that updates the selected options for a product. You'll trigger this function when the customer selects an option.

Finally, you define an `allVariantsSelected` variable that indicates whether all variants are selected.

#### c. Handle Add to Cart

Next, you'll add a function that is triggered when the add-to-cart button is clicked.

Replace the `TODO` in the `BundleActions` component with the following code:

```tsx title="src/modules/products/components/bundle-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const handleAddToCart = async () => {
  if (!allVariantsSelected) {return}

  setIsAdding(true)
  await addBundleToCart({
    bundleId: bundle.id,
    quantity: 1,
    countryCode,
    items: bundle.items.map((item, index) => ({
      item_id: item.id,
      variant_id: selectedVariants[index]?.id ?? "",
    })),
  })
  setIsAdding(false)
}
```

The `handleAddToCart` function adds the bundle to the cart if all variants have been selected. It uses the `addBundleToCart` function you created in the previous step.

#### d. Customize the ProductPrice Component

Before you render the component in `BundleActions`, you'll make a small adjustment to the `ProductPrice` component to allow passing a CSS class.

In `src/modules/products/components/product-price/index.tsx`, add a `className` prop to the `ProductPrice` component:

```tsx title="src/modules/products/components/product-price/index.tsx" badgeLabel="Storefront" badgeColor="blue"
export default function ProductPrice({
  // ...
  className,
}: {
  // ...
  className?: string
}) {
  // ...
}
```

Then, in the `return` statement, pass the `className` prop in the classes of the first `span` child of the wrapper `div`:

```tsx title="src/modules/products/components/product-price/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"]]}
<div className="flex flex-col text-ui-fg-base">
  <span
    className={clx("text-xl-semi", {
      "text-ui-fg-interactive": selectedPrice.price_type === "sale",
    }, className)}
  >
   {/* ... */}
  </span>
</div>
```

#### e. Render the Component

Finally, you'll render the component that shows the bundle's items and allows the customer to select the product variant for each item.

Add the following `return` statement to the `BundleActions` component:

```tsx title="src/modules/products/components/bundle-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div className="flex flex-col gap-y-6 max-w-2xl mx-auto w-full">
    <h2 className="text-2xl">Items in Bundle</h2>
    <div className="grid gap-6">
      {bundle.items.map((item, index) => (
        <div 
          key={item.product.id} 
          className="rounded-lg p-6 shadow-elevation-card-rest hover:shadow-elevation-card-hover transition-shadow bg-white"
        >
          <div className="flex items-start gap-4">
            <Thumbnail
              thumbnail={item.product.thumbnail}
              className="w-24 h-24 rounded-md"
              size="square"
              images={[]}
            />
            <div>
              <h3 className="text-lg">{item.product.title}</h3>
              <ProductPrice
                product={item.product}
                variant={selectedVariants[index]}
                className="!text-sm mt-2 text-ui-fg-muted"
              />
            </div>
          </div>

          {(item.product.variants?.length ?? 0) > 1 && (
            <div className="space-y-4 mt-4">
              {(item.product.options || []).map((option) => (
                <div key={option.id}>
                  <OptionSelect
                    option={option}
                    current={productOptions[item.product.id]?.[option.id]}
                    updateOption={(optionId, value) =>
                      setOptionValue(item.product.id, optionId, value)
                    }
                    title={option.title ?? ""}
                    disabled={isAdding}
                  />
                </div>
              ))}
            </div>
          )}
        </div>
      ))}
    </div>

    <Button
      onClick={handleAddToCart}
      disabled={!allVariantsSelected || isAdding}
      variant="primary"
      className="w-full h-10"
      isLoading={isAdding}
    >
      {!allVariantsSelected ? "Select all variants" : "Add bundle to cart"}
    </Button>
  </div>
)
```

You show the bundle's items in cards. For each item, you show the product's thumbnail, title, and price.

If a product has multiple options, you show the options as buttons that the customer can select from.

Finally, you show an add-to-cart button that is disabled if not all items have selected variants, or if the bundle is being added to the cart.

### Use BundleActions Component in Product Page

You'll show the `BundleActions` component in the product page if the product is a bundled product.

First, in `src/modules/products/templates/product-actions-wrapper/index.tsx` add the following imports at the top of the file:

```tsx title="src/modules/products/templates/product-actions-wrapper/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { BundleProduct } from "@lib/data/products"
import BundleActions from "@modules/products/components/bundle-actions"
```

Then, add a `bundle` prop to the `ProductActionsWrapper` component:

```tsx title="src/modules/products/templates/product-actions-wrapper/index.tsx" badgeLabel="Storefront" badgeColor="blue"
export default async function ProductActionsWrapper({
  // ...
  bundle,
}: {
  // ...
  bundle?: BundleProduct
}) {
  // ...
}
```

Finally, add the following before the `ProductActionsWrapper` component's `return` statement:

```tsx title="src/modules/products/templates/product-actions-wrapper/index.tsx" badgeLabel="Storefront" badgeColor="blue"
if (bundle) {
  return <BundleActions bundle={bundle} />
}
```

This will show the `BundleActions` component if the `bundle` prop is set.

Next, you need to pass the `bundle` prop to the `ProductActionsWrapper` component.

In `src/modules/products/templates/index.tsx`, add the following import at the top of the file:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { BundleProduct } from "@lib/data/products"
```

And pass the `bundle` prop to the `ProductTemplate` component:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
type ProductTemplateProps = {
  // ...
  bundle?: BundleProduct
}

const ProductTemplate: React.FC<ProductTemplateProps> = ({
  // ...
  bundle,
}) => {
  // ...
}
```

Then, in the `ProductTemplate` component's `return` statement, find the `ProductActionsWrapper` component and pass the `bundle` prop to it:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
<ProductActionsWrapper
  // ...
  bundle={bundle}
/>
```

Lastly, you need to pass the `bundle` prop to the `ProductTemplate` component.

In `src/app/[countryCode]/(main)/products/[handle]/page.tsx`, add the `bundle` prop to `ProductTemplate` in the `ProductPage`'s `return` statement:

```tsx title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <ProductTemplate
    // ...
    bundle={bundleProduct?.bundle_product}
  />
)
```

You pass the bundle using the `bundleProduct` variable you declared earlier.

### Test it Out

To test it out, start the Medusa application by running the following command in the Medusa application's directory:

```bash npm2yarn badgeLabel="Medusa application" badgeColor="green"
npm run dev
```

Then, start the Next.js Starter Storefront by running the following command in the storefront's directory:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

Next, open the storefront in your browser at `http://localhost:8000`, click on Menu at the top right, then choose Store from the menu.

This will open the product catalogue page, showing the product associated with your bundled product.

If you can't see the product associated with your bundled product, make sure you've added it to the default sales channel (or the sales channel you use in your storefront), as explained in the [Edit Associated Product](#edit-associated-product) section.

The items in the bundle must also be added to the same sales channel.

![Product catalogue page with the bundled product showing](https://res.cloudinary.com/dza7lstvk/image/upload/v1745926664/Medusa%20Resources/Screenshot_2025-04-29_at_2.37.22_PM_ktk4e5.png)

If you click on the bundled product, you can see in its details page the items in the bundle.

![Bundled products detail page showing the items](https://res.cloudinary.com/dza7lstvk/image/upload/v1745926778/Medusa%20Resources/Screenshot_2025-04-29_at_2.39.16_PM_mskh01.png)

Once you select the necessary options for all products in the bundle, the "Add to cart" button will be enabled. You can click on it to add the bundle's items to the cart.

![Cart with the bundled items in it](https://res.cloudinary.com/dza7lstvk/image/upload/v1745929244/Medusa%20Resources/Screenshot_2025-04-29_at_3.20.27_PM_qnbdds.png)

You can then place an order with the bundled items. Then, on the Medusa Admin dashboard, you can fulfill and process the items separately.

![Example of fulfilling one item in the bundle](https://res.cloudinary.com/dza7lstvk/image/upload/v1745929701/Medusa%20Resources/Screenshot_2025-04-29_at_3.22.02_PM_nvrvvz.png)

***

## Step 12: Remove Bundle from Cart

The last functionality you'll implement is the ability to remove a bundled product from the cart. When a customer chooses to remove an item in the cart that's part of a bundle, you should remove all items in the bundle from the cart.

To implement this, you need:

- A workflow that implements the logic to remove a bundle's items from the cart.
- An API route that exposes the workflow's functionality to storefronts.
- A function in the storefront that calls the API route to remove the bundle from the cart.

### Create Remove Bundle from Cart Workflow

You'll start by creating a workflow that implements the logic to remove a bundle's items from the cart.

The workflow has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the details of the cart and its items.
- [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md): Acquire a lock on the cart to prevent concurrent modifications.
- [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/index.html.md): Remove the items in the bundle from the cart.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the updated cart.
- [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md): Release the lock on the cart.

Medusa provides all these steps and workflows in its `@medusajs/medusa/core-flows` package. So, you can create the workflow right away.

Create the file `src/workflows/remove-bundle-from-cart.ts` with the following content:

```ts title="src/workflows/remove-bundle-from-cart.ts" collapsibleLines="1-12" expandButtonLabel="Show Imports" highlights={removeBundleFromCartWorkflowHighlights}
import { 
  createWorkflow, 
  transform, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  acquireLockStep,
  deleteLineItemsWorkflow, 
  releaseLockStep,
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"

type RemoveBundleFromCartWorkflowInput = {
  bundle_id: string
  cart_id: string
}

export const removeBundleFromCartWorkflow = createWorkflow(
  "remove-bundle-from-cart",
  ({ bundle_id, cart_id }: RemoveBundleFromCartWorkflowInput) => {
    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      fields: [
        "*",
        "items.*",
      ],
      filters: {
        id: cart_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const itemsToRemove = transform({
      cart: carts[0],
      bundle_id,
    }, (data) => {
      return data.cart.items.filter((item) => {
        return item?.metadata?.bundle_id === data.bundle_id
      }).map((item) => item!.id)
    })

    acquireLockStep({
      key: cart_id,
      timeout: 2,
      ttl: 10,
    })

    deleteLineItemsWorkflow.runAsStep({
      input: {
        cart_id,
        ids: itemsToRemove,
      },
    })

    // retrieve cart again
    const { data: updatedCarts } = useQueryGraphStep({
      entity: "cart",
      fields: [
        "*",
        "items.*",
      ],
      filters: {
        id: cart_id,
      },
    }).config({ name: "retrieve-cart" })
    
    releaseLockStep({
      key: cart_id,
    })
    
    return new WorkflowResponse(updatedCarts[0])
  }
)
```

The workflow accepts as an input the bundle's ID and the cart's ID.

In the workflow, you:

- Retrieve the cart and its items using the `useQueryGraphStep`.
- Use `transform` to filter the items in the cart and return only the IDs of the items that belong to the bundle.
- Acquire a lock on the cart using the `acquireLockStep` to prevent concurrent modifications.
- Remove the items from the cart using the `deleteLineItemsWorkflow`.
- Retrieve the updated cart using the `useQueryGraphStep`.
- Release the lock on the cart using the `releaseLockStep`.

Finally, you return the updated cart.

### Create API Route

Next, you'll create the API route that exposes the workflow's functionality to storefronts.

Create the file `src/api/store/carts/[id]/line-item-bundles/[bundle_id]/route.ts` with the following content:

```ts title="src/api/store/carts/[id]/line-item-bundles/[bundle_id]/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { 
  removeBundleFromCartWorkflow,
} from "../../../../../../workflows/remove-bundle-from-cart"

export async function DELETE(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result: cart } = await removeBundleFromCartWorkflow(req.scope)
    .run({
      input: {
        cart_id: req.params.id,
        bundle_id: req.params.bundle_id,
      },
    })

  res.json({
    cart,
  })
}
```

You export a `DELETE` route handler, which exposes a `DELETE` API route at `/store/carts/:id/line-item-bundles/:bundle_id`.

In the route handler, you execute the `removeBundleFromCartWorkflow` workflow to delete the bundle's items from the cart.

Finally, you return the updated cart in the response.

### Add Remove Bundle from Cart in Storefront

You'll now customize the storefront to add a button that removes the bundle from the cart.

Start by adding the following function at the end of `src/lib/data/cart.ts`:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
export async function removeBundleFromCart(bundleId: string) {
  const cartId = await getCartId()
  const headers = {
    ...(await getAuthHeaders()),
  }
 
  await sdk.client.fetch<HttpTypes.StoreCartResponse>(
    `/store/carts/${cartId}/line-item-bundles/${bundleId}`, 
    {
      method: "DELETE",
      headers,
    }
  )
    .then(async () => {
      const cartCacheTag = await getCacheTag("carts")
      revalidateTag(cartCacheTag)

      const fulfillmentCacheTag = await getCacheTag("fulfillment")
      revalidateTag(fulfillmentCacheTag)
    })
    .catch(medusaError)
}
```

You define the `removeBundleFromCart` function that sends a `DELETE` request to the API route you created in the previous step.

Next, you'll update the delete button used in the cart UI to call the `removeBundleFromCart` function when removing a bundle item from the cart.

In `src/modules/common/components/delete-button/index.tsx`, add the following import at the top of the file:

```tsx title="src/modules/common/components/delete-button/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { removeBundleFromCart } from "@lib/data/cart"
```

Then, add a `bundle_id` prop to the `DeleteButton` component:

```tsx title="src/modules/common/components/delete-button/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const DeleteButton = ({
  // ...
  bundle_id,
}: {
  // ...
  bundle_id?: string
}) => {
  // ...
}
```

Finally, replace the `handleDelete` function in the `DeleteButton` component with the following:

```tsx title="src/modules/common/components/delete-button/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const handleDelete = async (id: string) => {
  setIsDeleting(true)
  if (bundle_id) {
    await removeBundleFromCart(bundle_id).catch((err) => {
      setIsDeleting(false)
    })
  } else {
    await deleteLineItem(id).catch((err) => {
      setIsDeleting(false)
    })
  }
}
```

If the `bundle_id` prop is set, the `handleDelete` function calls the `removeBundleFromCart` function. Otherwise, it calls the default `deleteLineItem` function.

Next, you'll update the components using the `DeleteButton` component to pass the `bundle_id` prop.

In `src/modules/cart/components/item/index.tsx`, find the `DeleteButton` component in the `return` statement and replace it with the following:

```tsx title="src/modules/cart/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"]]}
<DeleteButton 
  id={item.id} 
  data-testid="product-delete-button" 
  bundle_id={item.metadata?.bundle_id as string}
>
  {item.metadata?.bundle_id !== undefined ? "Remove bundle" : "Remove"}
</DeleteButton>
```

You pass the `bundle_id` prop to the `DeleteButton` component, which is set to the item's metadata. You also change the text based on whether the item is in a bundle.

Then, in `src/modules/layout/components/cart-dropdown/index.tsx`, find the `DeleteButton` component in the `return` statement and replace it with the following:

```tsx title="src/modules/layout/components/cart-dropdown/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"]]}
<DeleteButton
  id={item.id}
  className="mt-1"
  data-testid="cart-item-remove-button"
  bundle_id={item.metadata?.bundle_id as string}
>
  {item.metadata?.bundle_id !== undefined ? "Remove bundle" : "Remove"}
</DeleteButton>
```

Similarly, you pass the `bundle_id` prop to the `DeleteButton` component and change the text based on whether the item is in a bundle.

### Test it Out

To test it out, start the Medusa application and the Next.js Starter Storefront as you did in the previous step.

Then, open the storefront in your browser at `http://localhost:8000`. Given you've already added a bundled product to the cart, you can now see a "Remove bundle" button next to the bundled product in the cart.

![Cart with the Remove bundle button showing for bundle items](https://res.cloudinary.com/dza7lstvk/image/upload/v1746011200/Medusa%20Resources/Screenshot_2025-04-30_at_2.06.02_PM_cgtg45.png)

If you click on the "Remove bundle" button for any of the bundle's items, all items in the bundle will be removed from the cart.

***

## Next Steps

Now that you have a working bundled product feature, you can customize it further to fit your use case:

- Add API routes to update the bundled product and its items in the cart.
- Add more CRUD management features to the Bundled Products page in the Medusa Admin.
- Customize the Next.js Starter Storefront to show the bundled products together in the cart, rather than separately.
- Use custom logic to set the price of the bundled product.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Bundled Products Recipe

This recipe provides the general steps to implement bundled products in your Medusa application.

Follow the step-by-step [Bundled Products Example](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/bundled-products/examples/standard/index.html.md) to learn how to implement bundled products in your Medusa application.

## Overview

Bundled products allow you to group multiple products into a single bundle that customers can purchase together. By using bundled products, you can offer items at a discounted price or fulfill items within the same bundle separately, among other features.

Medusa provides an [inventory kit](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md) feature that allows you to create bundled products. However, it doesn't support all bundled-product features. For example, you can't set a different price for the bundle, or fulfill items within the same bundle separately.

To support more bundled-product features, you can customize the Medusa application by creating a Bundled Product Module and building flows around it.

***

## Create Bundled Product Module

Your custom features and functionalities are implemented inside modules. The module is integrated into the Medusa application without any implications on existing functionalities.

The module will hold your custom data models and the service implementing bundled-product-related features.

[How to Create a Module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md): Learn how to create a module.

### Create Custom Data Models

A data model represents a table in the database. You can define in your module data models to store data related to your custom features, such as a bundled product.

For example, you can define:

- A `Bundle` model for the bundle itself.
- A `BundleItem` model for the items in the bundle.

Then, you can link your custom data model to data models from other modules. For example, you can link the `BundleItem` model to the Product Module's `Product` data model.

- [How to Create a Data Model](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md): Learn how to create a data model.
- [Define Module Links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md): Define links between data models.

### Implement Data Management Features

Your module’s main service holds data-management and other related features. Then, in other resources, such as an API route, you can resolve the service from the Medusa container and use its functionalities.

Medusa facilitates implementing data-management features using the service factory. Your module's main service can extend this service factory, and it generates data-management methods for your data models.

[Service Factory](https://docs.medusajs.com/docs/learn/fundamentals/modules/service-factory/index.html.md): Learn about the service factory and how to use it.

***

## Implement Workflows

You implement the features in your use case using workflows. A workflow is a series of queries and actions, called steps, that complete a task.

By using workflows, you benefit from features like rollback mechanism, error handling, and retrying failed steps.

You can implement workflows that create a bundled product, add bundled product to the cart, and more. In the workflow's steps, you can resolve the Bundled Product Module's service and use its data-management methods to manage bundled products.

Then, you can utilize these workflows in other resources, such as an API route.

[Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md): Learn how to create a workflow.

***

## Add Custom API Routes

API routes expose your features to external applications, such as the admin dashboard or the storefront.

You can create custom API routes that expose the features you've built as workflows. For example, you can create an API route that allows merchants to list and create bundled products.

[API Routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md): Learn how to create an API route.

***

## Manage Linked Records

If you've defined links between data models of two modules, you can manage them through two tools: Link and Query.

Use Link to create a link between two records, and use Query to fetch data across linked data models.

For example, you can define a link between a `Bundle` and a `Product` from the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md). Later, you can retrieve the product associated with the bundle using Query.

- [How to Use Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): Learn how to link data models of different modules.
- [How to Use Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md): Learn how to fetch data across modules with Medusa's Query.

***

## Customize Admin Dashboard

You can extend the Medusa Admin to provide merchants with an interface to manage bundled products. You can inject widgets into existing pages or create new pages.

In your customizations, you send requests to the API routes you created to manage bundled products.

- [Create a Widget](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md): Learn how to create a widget in the Medusa Admin.
- [Create UI Route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md): Learn how to create a UI route in the Medusa Admin.

***

## Customize or Build Storefront

Customers use your storefront to browse your bundled products and purchase them. You can also provide other helpful features, such as displaying the bundle's details and allowing customers to select options for the items in the bundle.

Medusa provides a Next.js Starter Storefront with standard commerce features including listing products, placing orders, and managing accounts. You can customize the storefront and cater its functionalities to support bundled products.

Alternatively, you can build the storefront with your preferred tech stack.

- [Next.js Starter Storefront](https://docs.medusajs.com/nextjs-starter/index.html.md): Learn how to install and use the Next.js Starter Storefront.
- [Storefront Guides](https://docs.medusajs.com/storefront-development/index.html.md): Learn how to build a storefront for your Medusa application.


# Commerce Automation Recipe

This recipe provides the general steps to implement commerce automation with Medusa.

## Overview

Commerce automation is essential for businesses to save costs, provide a better user experience, and avoid manual, repetitive tasks that lead to human errors. Businesses utilize automation in different domains, including marketing, customer support, and order management.

Medusa provides the necessary architecture and tools to implement commerce automation for order management, customer service, and more. You can perform an asynchronous action when an event is triggered, schedule a job that runs at a specified interval, and more.

***

## Re-Stock Notifications

Customers may be interested in a product that is currently out of stock. Instead of losing their interest, you can allow them to subscribe to receive a notification when the product is back in stock.

Then, you can listen to product-related events and notify subscribed customers when a product variant is back in stock.

The following guide explains how to add restock notifications in your Medusa application:

[Restock Notification Guide](https://docs.medusajs.com/recipes/commerce-automation/restock-notification/index.html.md): Learn how to implement restock notifications in the Medusa application.

***

## Automated Customer Support

Customer support is essential to build a store's brand and customer loyalty. However, to provide an efficient customer support, you often need to integrate with third-party services, like Zendesk, and automate customer notifications.

### Integrate with Third-Party Services

To provide customer support, you can Integrate with third-party services, such as ticket systems or chat bots in the storefront.

Medusa allows you to easily integrate with third-party services by creating a custom module, then build workflows for your business logic that perform actions with the third-party service.

This approach allows you to interact with the third-party service within custom and existing flows, while maintaining data consistency across systems. You can then execute the wokflow when an event is triggered, such as when a customer places an order or requests a return.

- [Create Module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md): Learn about how to create a custom module.
- [Create Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md): Learn how to create a workflow.

### Automate Customer Notifications

You can also automate sending notifications to customers when changes happen related to their orders, returns, exchanges, and more.

Medusa's Notification Module allows you to send notifications when an event is triggered, such as when a customer's order is updated. You can use third-party services, like [SendGrid](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md), to send emails to customers.

- [Notification Module](https://docs.medusajs.com/infrastructure-modules/notification/index.html.md): Learn about the Notification Module.
- [Create Subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md): Learn how to create a subscriber to handle events.

***

## Automatic Data Synchronization

As your commerce store grows, you'll likely need to synchronize data across different systems. For example, you need to synchronize data with an ERP system or a data warehouse.

Refer to the [ERP](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/erp/index.html.md) recipe for a focused guide on how to integrate with an ERP system.

To implement that, you can:

- Create a workflow that implements the synchronization steps, along with retry and rollback logic. By using a workflow, you ensure data consistency across systems.
- Create a scheduled job that executes the workflow automatically at the specified time pattern.

- [Create Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md): Learn how to create a workflow.
- [Create a Scheduled Job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md): Learn how to create a scheduled job.

***

## Order Management Automation

Medusa's architecture, Commerce Modules, and Framework for customizations facilitate automating a large amount of order management functionalities.

For example, you can automatically:

- Create a fulfillment when an order is placed.
- Create a refund when an item is returned.
- Send a notification when an order is shipped.

To handle events within an order flow and automate actions, you can create a subscriber that listens to the relevant event. For example, you can create a subscriber that listens to the `order.placed` event and automatically creates a fulfillment if predefined conditions are met.

- [Create a Subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md): Learn how to create a subscriber in Medusa.
- [Events Reference](https://docs.medusajs.com/references/events/index.html.md): Check out triggered events by each Commerce Module.

***

## Automated RMA Flow

Businesses must optimize their Return Merchandise Authorization (RMA) flow to ensure a good customer experience and service. By automating the flow, customers request to return their received items, and businesses quickly support them.

Medusa's commerce features are geared towards automating RMA flows and ensuring a good customer experience:

- Customers can create order returns from the storefront. Merchants then receive a notification and handle the return from the Medusa Admin.
- Merchants can make order changes and request the customer's approval for them. The customer can also send any additional payment if necessary.
- Every order-related action triggers an event, which you can listen to with a subscriber. This allows you to handle order events to automate actions.

- [Order Module](https://docs.medusajs.com/commerce-modules/order/index.html.md): Learn about the Order Module and its features.
- [Create a Subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md): Learn how to create a subscriber in Medusa.

***

## Customer Segmentation

Businesses use customer segmentation to organize customers into different groups and then apply different price rules to these groups.

Medusa's Commerce Modules provide the necessary features to implement this use case:

- The Customer Module allows you to organize customers into customer groups.
- The Pricing Module allows you to specify prices based on a condition, such as the group of the customer.

For example, to group customers with over twenty orders:

1. Create a subscriber that listens to the `order.placed` event.
2. If the customer has more than 20 orders, add them to the VIP customer group.

- [Customer Module](https://docs.medusajs.com/commerce-modules/customer/index.html.md): Learn about the Customer Module and its features.
- [Pricing Module](https://docs.medusajs.com/commerce-modules/pricing/index.html.md): Learn about the Pricing Module and its features.
- [Create a Subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md): Learn how to create a subscriber in Medusa.
- [Events Reference](https://docs.medusajs.com/references/events/index.html.md): Check out triggered events by each Commerce Module.

***

## Marketing Automation

In your commerce store, you may utilize marketing strategies that encourage customers to make purchases. For example, you send a newsletter when new products are added to your store.

To do that, create a subscriber that listens to the `product.created`, and send an email to subscribed customers with tools like [SendGrid](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md).

You can also create a scheduled job that checks whether the number of new products has exceeded a set threshold, then sends out the newsletter.

- [Create a Subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md): Learn how to create a subscriber in Medusa.
- [Scheduled Jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md): Learn how to create a scheduled job in Medusa.


# Implement Restock Notifications in Medusa

In this guide, you'll learn how to notify customers when a variant is restocked in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) which are available out-of-the-box. These features include managing the inventory of product variants in different stock locations and sales channels.

Customers browsing your store may be interested in a product that is currently out of stock. To keep the customer interested in your store and encourage them to purchase the product in the future, you can build customizations around Medusa's commerce features to subscribe customers to receive a notification when the product is restocked.

This guide will teach you how to:

- Install and set up Medusa.
- Implement the data model to subscribe for variant restocking.
- Add a custom endpoint to subscribe a customer to a variant's restock notification.
- Build a flow to send a notification to customers subscribed to a variant when it's restocked.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer.

[Example Repository](https://github.com/medusajs/examples/tree/main/restock-notification): Find the full code of the guide in this repository.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. You can also optionally choose to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credential and submit the form. Afterwards, you can login with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Restock Module

To add custom tables to the database, which are called data models, you create a module. A module is a re-usable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

In this step, you'll create a Restock Module that adds a custom data model for restock notification subscriptions. In later steps, you'll store customer subscriptions in this data model.

Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).

### Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/restock`.

![Diagram showcasing the module directory to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1733222736/Medusa%20Resources/restock-dir-overview-1_hiz58j.jpg)

### Create Data Models

A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

Learn more about data models in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md).

In Medusa, you have sales channels that indicate the channels you sell your products through, such as online storefront or offline store. A product's variants have different inventory quantities across stock locations, which are associated with sales channels.

![A diagram showcasing how a variant's inventory is stored across modules](https://res.cloudinary.com/dza7lstvk/image/upload/v1733224214/Medusa%20Resources/inventory-details-example_nvx4cj.jpg)

So, a customer sees the inventory quantity of a product variant based on their sales channel. To subscribe a customer to a product variant's restock notification, you'll store the subscription in a `RestockSubscription` data model.

You create a data model in a TypeScript or JavaScript file under the `models` directory of a module. So, create the file `src/modules/restock/models/restock-subscription.ts` with the following content:

![The directory structure of the Restock Module after adding this model.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733224503/Medusa%20Resources/restock-dir-overview-2_chap79.jpg)

```ts title="src/modules/restock/models/restock-subscription.ts"
import { model } from "@medusajs/framework/utils"

const RestockSubscription = model.define("restock_subscription", {
  id: model.id().primaryKey(),
  variant_id: model.text(),
  sales_channel_id: model.text(),
  email: model.text(),
  customer_id: model.text().nullable(),
})
.indexes([
  {
    on: ["variant_id", "sales_channel_id", "email"],
    unique: true,
  },
])

export default RestockSubscription
```

You define the data model using DML's `define` method. It accepts two parameters:

1. The first one is the name of the data model's table in the database.
2. The second is an object, which is the data model's schema. The schema's properties are defined using DML methods.

In the data model, you define the following properties:

1. `id`: A primary key ID for each record.
2. `variant_id`: The ID of a variant that customers have subscribed to.
3. `sales_channel_id`: The ID of the sales channel that this variant is out-of-stock in.
4. `email`: The email of the customer subscribed to the restock notification.
5. `customer_id`: The customer's ID in Medusa. This is nullable in case the customer is a guest.

Learn more about data model [properties](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md) and [relations](https://docs.medusajs.com/docs/learn/fundamentals/data-models/relationships/index.html.md).

You also define a unique index on the `variant_id`, `sales_channel_id`, and `email` properties using the `indexes` method.

Learn more about data model indexes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/index/index.html.md).

### Create Service

You define data-management methods of your data models in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can perform database operations.

Learn more about services in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md).

In this section, you'll create the Restock Module's service. Create the file `src/modules/restock/service.ts` with the following content:

![The directory structure of the Restock Module after adding this service.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733224957/Medusa%20Resources/restock-dir-overview-4_pkypup.jpg)

```ts title="src/modules/restock/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import RestockSubscription from "./models/restock-subscription"

class RestockModuleService extends MedusaService({
  RestockSubscription,
}) { }

export default RestockModuleService
```

The `RestockModuleService` extends `MedusaService` from the Modules SDK which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods.

So, the `RestockModuleService` class now has methods like `createRestockSubscriptions` and `retrieveRestockSubscription`.

Find all methods generated by the `MedusaService` in [this reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md).

You'll use this service in a later method to store and manage restock subscriptions.

### Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/restock/index.ts` with the following content:

![The directory structure of the Restock Module after adding the definition file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733225036/Medusa%20Resources/restock-dir-overview-5_dcam6u.jpg)

```ts title="src/modules/restock/index.ts"
import { Module } from "@medusajs/framework/utils"
import RestockModuleService from "./service"

export const RESTOCK_MODULE = "restock"

export default Module(RESTOCK_MODULE, {
  service: RestockModuleService,
})
```

You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `restock`.
2. An object with a required property `service` indicating the module's service.

### Add Module to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/restock",
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

### Generate Migrations

Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module.

Learn more about migrations in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md).

Medusa's CLI tool generates the migrations for you. To generate a migration for the Restock Module, run the following command in your Medusa application's directory:

```bash
npx medusa db:generate restock
```

The `db:generate` command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a `migrations` directory under `src/modules/restock` that holds the generated migration.

![The directory structure of the Restock Module after generating the migration](https://res.cloudinary.com/dza7lstvk/image/upload/v1733225157/Medusa%20Resources/restock-dir-overview-6_c2z6oi.jpg)

Then, to reflect these migrations on the database, run the following command:

```bash
npx medusa db:migrate
```

The table of the Restock Module's data model are now created in the database.

***

## Step 3: Link Restock Subscription to Product Variant

Since the `RestockSubscription` data model stores the product variant's ID, you may want to retrieve the product variant's details while retrieving a restock subscription record.

However, modules are [isolated](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md) to ensure they're re-usable and don't have side effects when integrated into the Medusa application. So, to build associations between modules, you define [module links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md). A Module link associates two modules' data models while maintaining module isolation.

In this section, you'll link the `RestockSubscription` data model to the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `ProductVariant` data model.

Learn more about module links in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md).

To define a link, create the file `src/links/restock-variant.ts` with the following content:

![The directory structure of the Medusa Application after adding this link.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733225402/Medusa%20Resources/restock-dir-overview-7_dln3fw.jpg)

```ts title="src/links/restock-variant.ts"
import { defineLink } from "@medusajs/framework/utils"
import RestockModule from "../modules/restock"
import ProductModule from "@medusajs/medusa/product"

export default defineLink(
  {
    linkable: RestockModule.linkable.restockSubscription.id,
    field: "variant_id",
  },
  ProductModule.linkable.productVariant,
  {
    readOnly: true,
  }
)
```

You define a link using `defineLink` from the Modules SDK. It accepts three parameters:

1. The first data model part of the link, which is the Restock Module's `restockSubscription` data model. A module has a special `linkable` property that contain link configurations for its data models. You also specify the field that points to the product variant.
2. The second data model part of the link, which is the Product Module's `productVariant` data model.
3. An object of configurations for the module link. By default, Medusa creates a table in the database to represent the link you define. However, in this guide, you only want this link to retrieve the variants associated with a subscription. So, you enable `readOnly` telling Medusa not to create a table for this link.

In the next steps, you'll see how this link allows you to retrieve product variants' details when retrieving restock subscriptions.

***

## Step 4: Create Restock Subscription Workflow

To subscribe customers to a variant's restock notification, you need a workflow.

A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an endpoint.

In this section, you'll create a workflow that validates that a variant is out-of-stock in the customer's sales channel, then subscribes the customer to the variant's restock notification. Later, you'll execute this workflow in an endpoint that you use in a storefront.

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md)

The workflow has the following steps:

- [validateVariantOutOfStockStep](#validatevariantoutofstockstep): Validate that the variant is out-of-stock, otherwise throw an error.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the restock subscription using Query if it exists.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the restock subscription using Query again to return it.

The `useQueryGraphStep` is from Medusa's workflows package. So, you'll only implement the other steps.

### validateVariantOutOfStockStep

The second step in the workflow will validate that the variant is actually out of stock in the customer's sales channel.

Create the file `src/workflows/create-restock-subscription/steps/validate-variant-out-of-stock.ts` with the following content:

![The directory structure of the Medusa application after adding the step.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733227547/Medusa%20Resources/restock-dir-overview-10_g3dbi3.jpg)

```ts title="src/workflows/create-restock-subscription/steps/validate-variant-out-of-stock.ts"
import { getVariantAvailability, MedusaError } from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

type ValidateVariantOutOfStockStepInput = {
  variant_id: string
  sales_channel_id: string
}

export const validateVariantOutOfStockStep = createStep(
  "validate-variant-out-of-stock",
  async ({ variant_id, sales_channel_id }: ValidateVariantOutOfStockStepInput, { container }) => {
    const query = container.resolve("query")
    const availability = await getVariantAvailability(query, {
      variant_ids: [variant_id],
      sales_channel_id,
    })
    
    if ((availability[variant_id].availability || 0) > 0) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Variant isn't out of stock."
      )
    }
  }
)
```

This step accepts the ID of the variant and the ID of the customer's sales channel. In the step, you use the `getVariantAvailability` from the Medusa Framework to get the variant's quantity in the specified sales channels. If the variant's quantity is greater than `0`, you throw an error, stopping the workflow's execution.

### createRestockSubscriptionStep

In the workflow, you'll try to retrieve the restock subscription if it already exists for the same email, variant ID, and sales channel ID. If it doesn't exist, you'll use this step to create the restock subscription.

Create the file `src/workflows/create-restock-subscription/steps/create-restock-subscription.ts` with the following content:

![The directory structure of the Medusa application after adding the step.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733227679/Medusa%20Resources/restock-dir-overview-11_dyrpao.jpg)

```ts title="src/workflows/create-restock-subscription/steps/create-restock-subscription.ts" highlights={createRSHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import RestockModuleService from "../../../modules/restock/service"
import { RESTOCK_MODULE } from "../../../modules/restock"

type CreateRestockSubscriptionStepInput = {
  variant_id: string
  sales_channel_id: string
  email: string
  customer_id?: string
}

export const createRestockSubscriptionStep = createStep(
  "create-restock-subscription",
  async (input: CreateRestockSubscriptionStepInput, { container }) => {
    const restockModuleService: RestockModuleService = container.resolve(
      RESTOCK_MODULE
    )

    const restockSubscription = await restockModuleService.createRestockSubscriptions(
      input
    )

    return new StepResponse(restockSubscription, restockSubscription)
  }
)
```

In the step, you resolve the Restock Module's service from the Medusa container. Medusa registers the service of custom and core modules in the container under the module's name.

Then, you use the service's `createRestockSubscriptions` method, which was generated by `MedusaService`, to create the restock subscription.

Learn more about a service's generated methods in [this reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md).

Finally, you return the created restock subscription by passing it as a first parameter to `StepResponse`. The second parameter is data passed to the compensation function, which you'll learn about next.

#### Add Compensation Function

A compensation function defines the rollback logic of a step, and it's only executed if an error occurs in the workflow. This eliminates data inconsistency if an error occurs and the workflow can't finish execution successfully.

Learn more about compensation functions in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/compensation-function/index.html.md).

Since the `createRestockSubscriptionStep` creates a restock subscription, you'll undo that in the compensation function. To add a compensation function, pass it as a third parameter to `createStep`:

```ts title="src/workflows/create-restock-subscription/steps/create-restock-subscription.ts"
export const createOrGetRestockSubscriptionsStep = createStep(
  // ...
  async (restockSubscription, { container }) => {
    if (!restockSubscription) {
      return
    }
    const restockModuleService: RestockModuleService = container.resolve(
      RESTOCK_MODULE
    )

    await restockModuleService.deleteRestockSubscriptions(restockSubscription.id)
  }
)
```

The compensation function receives two parameters:

1. The second parameter of `StepResponse`, which is the created restock subscription.
2. An object similar to the second parameter of a step function. It has a `container` property to resolve resources from the Medusa container.

In the compensation function, you resolve the Restock Module's service from the container, then delete the created subscription using the generated `deleteRestockSubscriptions` method.

### updateRestockSubscriptionStep

As mentioned in the previous step, the workflow will try to retrieve the restock subscription in case it already exists. If it does, you'll run this step to update its customer ID if it wasn't previously set in the subscription.

Create the file `src/workflows/create-restock-subscription/steps/update-restock-subscription.ts` with the following content:

![The directory structure of the Medusa application after adding the step.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733227952/Medusa%20Resources/restock-dir-overview-12_vubtkp.jpg)

```ts title="src/workflows/create-restock-subscription/steps/update-restock-subscription.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import RestockModuleService from "../../../modules/restock/service"
import { RESTOCK_MODULE } from "../../../modules/restock"

type UpdateRestockSubscriptionStepInput = {
  id: string
  customer_id?: string
}

export const updateRestockSubscriptionStep = createStep(
  "update-restock-subscription",
  async ({ id, customer_id }: UpdateRestockSubscriptionStepInput, { container }) => {
    const restockModuleService: RestockModuleService = container.resolve(
      RESTOCK_MODULE
    )

    const oldData = await restockModuleService.retrieveRestockSubscription(
      id
    )
    const restockSubscription = await restockModuleService.updateRestockSubscriptions({
      id,
      customer_id: oldData.customer_id || customer_id,
    })

    return new StepResponse(restockSubscription, oldData)
  },
  async (restockSubscription, { container }) => {
    if (!restockSubscription) {
      return
    }
    const restockModuleService: RestockModuleService = container.resolve(
      RESTOCK_MODULE
    )

    await restockModuleService.updateRestockSubscriptions(restockSubscription)
  }
)
```

In the step, you resolve the Restock Module's service and use its generated `retrieveRestockSubscription` method to retrieve the restock subscription. You then update the subscription with the `updateRestockSubscriptions`, updating the customer ID if it wasn't set in the subscription.

The step returns the updated restock subscription. It also passes to the compensation function the subscription's data before the update to undo the change in case an error occurs.

### Add createRestockSubscriptionWorkflow

You can now finally add the workflow that uses all these steps. Create the file `src/workflows/create-restock-subscription/index.ts` with the following content:

![The directory structure of the Medusa application after adding the workflow.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733229373/Medusa%20Resources/restock-dir-overview-13_zdwawe.jpg)

```ts title="src/workflows/create-restock-subscription/index.ts" highlights={subscriptionWorkflow1Highlights}
import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { validateVariantOutOfStockStep } from "./steps/validate-variant-out-of-stock"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { createRestockSubscriptionStep } from "./steps/create-restock-subscription"
import { updateRestockSubscriptionStep } from "./steps/update-restock-subscription"

type CreateRestockSubscriptionWorkflowInput = {
  variant_id: string
  sales_channel_id: string
  customer: {
    email?: string
    customer_id?: string
  }
}

export const createRestockSubscriptionWorkflow = createWorkflow(
  "create-restock-subscription",
  ({
    variant_id,
    sales_channel_id,
    customer,
  }: CreateRestockSubscriptionWorkflowInput) => {
    const customerId = transform({
      customer,
    }, (data) => {
      return data.customer.customer_id || ""
    })
    const retrievedCustomer = when(
      "retrieve-customer-by-id",
      { customer }, 
      ({ customer }) => {
        return !customer.email
      }
    ).then(() => {
      // @ts-ignore
      const { data } = useQueryGraphStep({
        entity: "customer",
        fields: ["email"],
        filters: { id: customerId },
        options: {
          throwIfKeyNotFound: true,
        },
      }).config({ name: "retrieve-customer" })

      return data
    })
    
    const email = transform({ 
      retrievedCustomer, 
      customer,
    }, (data) => {
      return data.customer?.email ?? data.retrievedCustomer?.[0].email
    })
    
    // TODO add more steps
  }
)
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function, which is the workflow's implementation. In the workflow, you:

- Use [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK to create a `customerId` variable. Its value is either the ID of the customer passed in the workflow's input if it's not `undefined`, or an empty string.
- Use [when-then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) from the Workflows SDK that performs steps if a condition is met. If the customer's email isn't set in the workflow's input, you retrieve the customer using `useQueryGraphStep` by its ID.
- Use `transform` again to create an `email` variable whose value is either the email passed in the workflow's input or the retrieved customer's email.

A workflow's constructor function has some constraints in implementation, which is why you need to use `transform` for variable manipulation and `when-then` to perform steps based on a condition. Learn more about these constraints in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md).

Next, replace the `TODO` with the following:

```ts title="src/workflows/create-restock-subscription/index.ts" highlights={subscriptionWorkflow2Highlights}
validateVariantOutOfStockStep({
  variant_id,
  sales_channel_id,
})

const { data: restockSubscriptions } = useQueryGraphStep({
  entity: "restock_subscription",
  fields: ["*"],
  filters: {
    email,
    variant_id,
    sales_channel_id,
  },
}).config({ name: "retrieve-subscriptions" })

when({ restockSubscriptions }, ({ restockSubscriptions }) => {
  return restockSubscriptions.length === 0
})
.then(() => {
  createRestockSubscriptionStep({
    variant_id,
    sales_channel_id,
    email,
    customer_id: customer.customer_id,
  })
})

when({ restockSubscriptions }, ({ restockSubscriptions }) => {
  return restockSubscriptions.length > 0
})
.then(() => {
  updateRestockSubscriptionStep({
    id: restockSubscriptions[0].id,
    customer_id: customer.customer_id,
  })
})

const { data: restockSubscription } = useQueryGraphStep({
  entity: "restock_subscription",
  fields: ["*"],
  filters: {
    email,
    variant_id,
    sales_channel_id,
  },
}).config({ name: "retrieve-restock-subscription" })

return new WorkflowResponse(
  restockSubscription
)
```

You add the following steps to the workflow:

- `validateVariantOutOfStockStep` to validate that the variant is out of stock in the specified sales channel. If not, an error is thrown, halting the workflow's execution.
- `useQueryGraphStep` to retrieve the restock subscription in case it already exists.
- Use [when-then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) to perform an action if a condition is met.
  - The first when-then block checks if the restock subscription doesn't exist, then creates it using the `createRestockSubscriptionStep`.
  - The second when-then block checks if the restock subscription already exists, then updates it using the `updateRestockSubscriptionStep`.
- `useQueryGraphStep` again to retrieve the restock subscription before returning it.

Workflows must return an instance of `WorkflowResponse`, passing as a parameter the data to return to the workflow's executor. The workflow returns the restock subscription.

You'll execute the workflow when you create the API route next.

***

## Step 5: Subscribe to Restock Notifications API Route

Now that you implemented the flow to subscribe customers to a variant's restock notifications, you'll expose this feature through an API route.

An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts. You'll create an API route at the path `/store/restock-subscriptions` that executes the workflow from the previous step.

Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

### Implement API Route

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory.

The path of the API route is the file's path relative to `src/api`. So, to create the `/store/restock-subscriptions` API route, create the file `src/api/store/restock-subscriptions/route.ts` with the following content:

![The directory structure of the Medusa application after adding the route file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733230210/Medusa%20Resources/restock-dir-overview-16_sv7yk2.jpg)

```ts title="src/api/store/restock-subscriptions/route.ts" highlights={routeHighlights}
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
import { createRestockSubscriptionWorkflow } from "../../../workflows/create-restock-subscription"

type PostStoreCreateRestockSubscription = {
  variant_id: string
  email?: string
  sales_channel_id?: string
}

export async function POST(
  req: AuthenticatedMedusaRequest<PostStoreCreateRestockSubscription>,
  res: MedusaResponse
) {
  const salesChannelId = req.validatedBody.sales_channel_id || (
    req.publishable_key_context?.sales_channel_ids?.length ? 
      req.publishable_key_context?.sales_channel_ids[0] : undefined
  )
  if (!salesChannelId) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "At least one sales channel ID is required, either associated with the publishable API key or in the request body."
    )
  }
  const { result } = await createRestockSubscriptionWorkflow(req.scope)
    .run({
      input: {
        variant_id: req.validatedBody.variant_id,
        sales_channel_id: salesChannelId,
        customer: {
          email: req.validatedBody.email,
          customer_id: req.auth_context?.actor_id,
        },
      },
    })

  return res.sendStatus(200)
}

```

Since you export a `POST` function in this file, you're exposing a `POST` API route at `/store/restock-subscriptions`. The route handler function accepts two parameters:

1. A request object with details and context on the request, such as body parameters or authenticated customer details.
2. A response object to manipulate and send the response.

`AuthenticatedMedusaRequest` accepts the request body's type as a type argument.

In the function, you first declare the sales channel ID either based on the parameter specified in the request body or the publishable API key's first sales channel. If the sales channel's ID is not set, an error is thrown.

Then, you execute the `createRestockSubscriptionWorkflow` by invoking it, passing it the Medusa container which is stored in the `scope` property of the request object, and invoking its `run` method.

The `run` method accepts an object having an `input` property, which is the input to pass to the workflow. You pass the following input:

1. `variant_id`: The ID of the variant the customer is subscribing to. You access the request body parameters from the `validatedBody` property of the request object.
2. `sales_channel_id`: The ID of the sales channel.
3. `customer`: The subscriber customer's details:
   - `email`: The email passed in the request body, if available.
   - `customer_id`: The ID of the customer if they're authenticated.

Finally, you return a `201` response code, indicating that the customer has subscribed to restock notifications of the specified variant.

### Add Validation Schema

The API route accepts the variant ID, and optionally the customer email and sales channel ID as request body parameters. So, you'll create a schema to validate the request body.

In Medusa, you create validation schemas using [Zod](https://zod.dev/) in a TypeScript file under the `src/api` directory. So, create the file `src/api/store/restock-subscriptions/validators.ts` with the following content:

![The directory structure of the Medusa application after adding the validator file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733229734/Medusa%20Resources/restock-dir-overview-14_au0h15.jpg)

```ts title="src/api/store/restock-subscriptions/validators.ts"
import { z } from "zod"

export const PostStoreCreateRestockSubscription = z.object({
  variant_id: z.string(),
  email: z.string().optional(),
  sales_channel_id: z.string().optional(),
})
```

You create an object schema with the following properties:

- `variant_id`: A required string parameter.
- `email`: An optional string parameter. The email is optional if the customer is authenticated.
- `sales_channel_id`: An optional string parameter. By default, every route starting with `/store` must pass the publishable API key, which is linked to one or more sales channels. This parameter takes a precedence over the publishable API key's channel.

Learn more about creating schemas in [Zod's documentation](https://zod.dev/).

You can now replace the `PostStoreCreateRestockSubscription` type in `src/api/store/restock-subscriptions/route.ts` with the following:

```ts title="src/api/store/restock-subscriptions/route.ts"
// ...
import { z } from "zod"
import { PostStoreCreateRestockSubscription } from "./validators"

type PostStoreCreateRestockSubscription = z.infer<
  typeof PostStoreCreateRestockSubscription
>
// ...
```

Next, you'll use this schema for validation.

### Add Validation and Auth Middlewares

To use the Zod schema for validation, you apply the `validateAndTransformBody` middleware on the `/store/restock-subscriptions` route. A middleware is a function executed before the API route when a request is sent to it.

Learn more about middlewares in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

To apply middlewares, create the file `src/api/middlewares.ts` with the following content:

![The directory structure of the Medusa application after adding the middlewares file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733229866/Medusa%20Resources/restock-dir-overview-15_xvwkc0.jpg)

```ts title="src/api/middlewares.ts" highlights={middlewaresHighlights}
import { 
  authenticate, 
  defineMiddlewares, 
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { 
  PostStoreCreateRestockSubscription,
} from "./store/restock-subscriptions/validators"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/store/restock-subscriptions",
      method: "POST",
      middlewares: [
        authenticate("customer", ["bearer", "session"], {
          allowUnauthenticated: true,
        }),
        validateAndTransformBody(PostStoreCreateRestockSubscription),
      ],
    },
  ],
})
```

In this file, you export the middlewares definition using `defineMiddlewares` from the Medusa Framework. This function accepts an object having a `routes` property, which is an array of middleware configurations to apply on routes.

You pass in the `routes` array an object having the following properties:

- `matcher`: The route to apply the middleware on.
- `method`: The HTTP method to apply the middleware on for the specified API route.
- `middlewares`: An array of the middlewares to apply. You apply two middlewares:
  - `authenticate`: A middleware that guards and attaches the logged-in customer details to the request object received by the API route handler. The middleware accepts three parameters:
    - The type of user to authenticate, which is `customer`.
    - The types of authentication methods allowed.
    - An optional object of options. You enable the `allowUnauthenticated`, which allows both authenticated and guest customers to access the route, and attaches the authenticated customer's ID to the request object.
  - `validateAndTransformBody`: A middleware to ensure the received request body is valid against the Zod schema you defined earlier.

Any request sent to `/store/restock-subscriptions` will now automatically fail if its body parameters don't match the `PostStoreCreateRestockSubscription` validation schema.

### Test API Route

To test out this API route, start the Medusa application by running the following command in the root directory of the Medusa application:

```bash npm2yarn
npm run dev
```

Before sending the request, you need to obtain a publishable API key. So, open the Medusa Admin at `http://localhost:9000/app` and log in with the user you created earlier.

To access your application's API keys in the admin, go to Settings -> Publishable API Keys. You'll have an API key created by default, which is associated with the default sales channel. You can use this publishable API key in the request header.

![In the admin, click on Publishable API key in the sidebar. A table will show your API keys and allow you to create one.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733230421/Medusa%20Resources/Screenshot_2024-12-03_at_2.53.07_PM_gau9jy.png)

Then, to obtain an ID of a variant that's out of stock, access a product from the Products page and:

1. Under Variants, click on the variant you want to edit its inventory quantity.

![The variants table shows a product's variants. Click on a variant to open its details page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733230534/Medusa%20Resources/Screenshot_2024-12-03_at_2.55.11_PM_c1ml9l.png)

2. Under Inventory Items, click on an inventory item.

![The inventory items table shows the variant's items. Click on an item to open its details page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733230649/Medusa%20Resources/Screenshot_2024-12-03_at_2.57.01_PM_ccc9of.png)

3. Under Locations, click on the third-dots icon at the right of a location, then choose Edit from the dropdown.

![The locations are shown in a table. Click on the three-dots at a location's right side, then choose Edit from the dropdown.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733230730/Medusa%20Resources/Screenshot_2024-12-03_at_2.58.18_PM_waeepw.png)

4. In the drawer form, enter `0` for the item's in-stock quantity.
5. Click the Save button.

![In the drawer form, enter 0 in the In stock field, then click the Save button at the bottom.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733230822/Medusa%20Resources/Screenshot_2024-12-03_at_2.59.48_PM_vqaige.png)

6. Go back to the variant's page and click on the icon at the right of the JSON section.

![Click on the icon at the right of the JSON section.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733230912/Medusa%20Resources/Screenshot_2024-12-03_at_3.01.28_PM_bcau0e.png)

7. In the JSON object, hover over the `id` field and click the copy icon.

![Click on the copy icon next to the ID field.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733230981/Medusa%20Resources/Screenshot_2024-12-03_at_3.02.34_PM_ujyv5w.png)

Finally, send a `POST` request to the `/store/restock-subscriptions` API route:

```bash
curl -X POST http://localhost:9000/store/restock-subscriptions \
-H 'x-publishable-api-key: {api_key}' \
--data '{
    "variant_id": "{variant_id}",
    "email": "customer@gmail.com"
}'
```

Make sure to replace `{api_key}` with the publishable API key you copied from the settings, and `{variant_id}` for the ID of the out-of-stock variant.

You'll receive a `201` response, indicating that the guest customer with email `customer@gmail.com` is now subscribed to restock notifications for the specified variant in the first sales channel associated with the specified publishable API key.

In the next step, you'll implement the functionality to send a notification to the variant's subscribers when it's restocked.

***

## Step 6: Send Restock Notification Workflow

After allowing customers to subscribe to a variant's restock notification, you want to implement the flow that checks the variant is restocked and sends a notification to its subscribers.

In this step, you'll create a workflow that retrieves all restock subscriptions, checks which variants are now restocked, and sends a notification to their subscribers.

The workflow has the following steps:

- [getDistinctSubscriptionsStep](#getDistinctSubscriptionsStep): Retrieve restock subscriptions for distinct variant ID and sales channel pairings.
- [getRestockedStep](#getrestockedstep): Filter out the restock subscriptions to retrieve only ones for restocked variants.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve restocked subscriptions for all subscribers using Query.
- [sendRestockNotificationStep](#sendrestocknotificationstep): Send a notification to the subscribers of restock subscriptions.
- [deleteRestockSubscriptionStep](#deleterestocksubscriptionstep): Delete the restock subscriptions from the database.

The `useQueryGraphStep` is from Medusa's workflows. So, you'll only implement the other steps.

### Optional Prerequisite: Notification Module Provider

Within this workflow, you'll use Medusa's [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) to send an email to the customer.

The module delegates the email sending to a module provider, such as [SendGrid](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md) or [Resend](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/integrations/guides/resend/index.html.md). You can refer to their linked guides to set up either module providers.

Alternatively, for development and debugging purposes, you can use the default Notification Module Provider that only logs a message in the terminal instead of sending an email. To do that, add the following to the `modules` array in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    // ...
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/notification-local",
            id: "local",
            options: {
              channels: ["email", "feed"],
            },
          },
        ],
      },
    },
  ],
})

```

## getDistinctSubscriptionsStep

The first step is to retrieve all restock subscriptions to later check which variants have been restocked in their sales channel. However, considering there could be a lot of subscribers to the same variant and sales channel pairing, you'll retrieve subscriptions with distinct variant and sales channel ID pairings.

Before adding the step that does this, you'll add a method in the `RestockModuleService` to retrieve the distinct records from the database. So, add the following to `src/modules/restock/service.ts`:

```ts title="src/modules/restock/service.ts"
// other imports...
import { InjectManager, MedusaContext } from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@medusajs/framework/mikro-orm/knex"

class RestockModuleService extends MedusaService({
  RestockSubscription,
}) {
  // ...
  @InjectManager()
  async getUniqueSubscriptions(
    @MedusaContext() context: Context<EntityManager> = {}
  ) {
    return await context.manager?.createQueryBuilder("restock_subscription")
      .select(["variant_id", "sales_channel_id"]).distinct().execute()
  }
}

export default RestockModuleService
```

To perform queries on the database in a method, add the `@InjectManager` decorator to the method. This will inject a [forked MikroORM entity manager](https://mikro-orm.io/docs/identity-map#forking-entity-manager) that you can use in your method.

Methods with the `@InjectManager` decorator accept as a last parameter a context object that has the `@MedusaContext` decorator. The entity manager is injected into the `manager` property of this parameter.

In the method, you use the `createQueryBuilder` to construct a query, passing it the name of the `RestockSubscription`'s table. You then select distinct `variant_id` and `sales_channel` pairings, and execute and return the query's result.

You'll use this method in the step. To create the step, create the file `src/workflows/send-restock-notifications/steps/get-distinct-subscriptions.ts` with the following content:

![Directory structure of the Medusa application after adding the step.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733399774/Medusa%20Resources/restock-dir-overview-22_kzchmm.jpg)

```ts title="src/workflows/send-restock-notifications/steps/get-distinct-subscriptions.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import RestockModuleService from "../../../modules/restock/service"
import { RESTOCK_MODULE } from "../../../modules/restock"

export const getDistinctSubscriptionsStep = createStep(
  "get-distinct-subscriptions",
  async (_, { container }) => {
    const restockModuleService: RestockModuleService = container.resolve(
      RESTOCK_MODULE
    )

    const distinctSubscriptions = await restockModuleService.getUniqueSubscriptions()

    return new StepResponse(distinctSubscriptions)
  }
)
```

In the step, you resolve the Restock Module's service and use the `getUniqueSubscriptions` method to retrieve the distinct subscriptions. You return those subscriptions in the `StepResponse`.

### getRestockedStep

The second step of the workflow receives all restock subscriptions and returns only those whose variants are restocked in the specified sales channel.

Create the file `src/workflows/send-restock-notifications/steps/get-restocked.ts` with the following content:

![The directory structure of the Medusa application after adding the step.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733234506/Medusa%20Resources/restock-dir-overview-17_pdtees.jpg)

```ts title="src/workflows/send-restock-notifications/steps/get-restocked.ts"
import { getVariantAvailability, promiseAll } from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

type GetRestockedStepInput = {
  variant_id: string
  sales_channel_id: string
}[]

export const getRestockedStep = createStep(
  "get-restocked",
  async (input: GetRestockedStepInput, { container }) => {
    const restocked: GetRestockedStepInput = []
    const query = container.resolve("query")
    
    await promiseAll(
      input.map(async (restockSubscription) => {
        const variantAvailability = await getVariantAvailability(query, {
          variant_ids: [restockSubscription.variant_id],
          sales_channel_id: restockSubscription.sales_channel_id,
        })

        if ((variantAvailability[restockSubscription.variant_id].availability || 0) > 0) {
          restocked.push(restockSubscription)
        }
      })
    )

    return new StepResponse(restocked)
  }
)
```

In this step, you loop over the restock subscriptions and use `getVariantAvailability` from the Medusa Framework to retrieve a variant's quantity in the sales channel.

If the variant isn't out of stock, then the restock subscription is pushed into the `restocked` array, which is returned in the step's response.

### sendRestockNotificationStep

The third step of the workflow receives the subscriptions whose variants have been restocked to send a notification to their subscribers.

Create the file `src/workflows/send-restock-notifications/steps/send-restock-notification.ts` with the following content:

![The directory structure of the Medusa application after adding the step.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733234507/Medusa%20Resources/restock-dir-overview-18_uvlu0l.jpg)

```ts title="src/workflows/send-restock-notifications/steps/send-restock-notification.ts"
import { promiseAll } from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { InferTypeOf, ProductVariantDTO } from "@medusajs/framework/types"
import RestockSubscription from "../../../modules/restock/models/restock-subscription"

type SendRestockNotificationStepInput = (InferTypeOf<typeof RestockSubscription> & {
  product_variant?: ProductVariantDTO
})[]

export const sendRestockNotificationStep = createStep(
  "send-restock-notification",
  async (input: SendRestockNotificationStepInput, { container }) => {
    const notificationModuleService = container.resolve("notification")

    const notificationData = input.map((subscription) => ({
      to: subscription.email,
      channel: "email",
      template: "variant-restock",
      data: {
        variant: subscription.product_variant,
      },
    }))

    await notificationModuleService.createNotifications(notificationData)
  }
)
```

This step resolves the Notification Module's service from the Medusa container and, for each subscription, sends a notification to its subscribers.

To send a notification, you use the `createNotifications` method of the Notification Module's service. It accepts an array of notification objects, each having the following properties:

- `to`: The email to send the notification to.
- `channel`: The channel to send the notification through, which is `email` for sending an email.
- `template`: The email template to use for this notification.
- `data`: Data to pass to the template relevant for the notification. Since the email will probably include details about the variant, you pass the variant's details.

### deleteRestockSubscriptionStep

The final step deletes the restock subscriptions whose subscribers have been notified.

Create the file `src/workflows/send-restock-notifications/steps/delete-restock-subscriptions.ts` with the following content:

![The directory structure of the Medusa application after adding the step.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733234506/Medusa%20Resources/restock-dir-overview-19_qfospx.jpg)

```ts title="src/workflows/send-restock-notifications/steps/delete-restock-subscriptions.ts"
import { InferTypeOf } from "@medusajs/framework/types"
import RestockSubscription from "../../../modules/restock/models/restock-subscription"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import RestockModuleService from "../../../modules/restock/service"
import { RESTOCK_MODULE } from "../../../modules/restock"

type DeleteRestockSubscriptionsStepInput = InferTypeOf<typeof RestockSubscription>[]

export const deleteRestockSubscriptionStep = createStep(
  "delete-restock-subscription",
  async (
    restockSubscriptions: DeleteRestockSubscriptionsStepInput, 
    { container }
  ) => {
    const restockModuleService: RestockModuleService = container.resolve(
      RESTOCK_MODULE
    )

    await restockModuleService.deleteRestockSubscriptions(
      restockSubscriptions.map((subscription) => subscription.id)
    )
    
    return new StepResponse(undefined, restockSubscriptions)
  },
  async (restockSubscriptions, { container }) => {
    if (!restockSubscriptions) {
      return
    }
    
    const restockModuleService: RestockModuleService = container.resolve(
      RESTOCK_MODULE
    )

    await restockModuleService.createRestockSubscriptions(restockSubscriptions)
  }
)
```

In the step, you resolve the Restock Module's service and use its `deleteRestockSubscriptions` to delete the restock subscriptions.

In the step's compensation, which receives the deleted restock subscriptions as a parameter, you resolve the Restock Module's service and use its `createRestockSubscriptions` to create these subscriptions again if an error occurs.

### Implement sendRestockNotificationsWorkflow

You can now implement the workflow that sends restock notifications using the above steps.

Create the file `src/workflows/send-restock-notifications/index.ts` with the following content:

![The directory structure of the Medusa application after adding the workflow.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733234507/Medusa%20Resources/restock-dir-overview-20_mcqkkx.jpg)

```ts title="src/workflows/send-restock-notifications/index.ts"
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { getRestockedStep } from "./steps/get-restocked"
import { sendRestockNotificationStep } from "./steps/send-restock-notification"
import { deleteRestockSubscriptionStep } from "./steps/delete-restock-subscriptions"
import { getDistinctSubscriptionsStep } from "./steps/get-distinct-subscriptions"

export const sendRestockNotificationsWorkflow = createWorkflow(
  "send-restock-notifications",
  () => {
    const subscriptions = getDistinctSubscriptionsStep()

    // @ts-ignore
    const restockedSubscriptions = getRestockedStep(subscriptions)

    const { variant_ids, sales_channel_ids } = transform({
      restockedSubscriptions,
    }, (data) => {
      const filters: Record<string, string[]> = {
        variant_ids: [],
        sales_channel_ids: [],
      }
      data.restockedSubscriptions.map((subscription) => {
        filters.variant_ids.push(subscription.variant_id)
        filters.sales_channel_ids.push(subscription.sales_channel_id)
      })

      return filters
    })

    const { data: restockedSubscriptionsWithEmails } = useQueryGraphStep({
      entity: "restock_subscription",
      fields: ["*", "product_variant.*"],
      filters: {
        variant_id: variant_ids,
        sales_channel_id: sales_channel_ids,
      },
    })

    // @ts-ignore
    sendRestockNotificationStep(restockedSubscriptionsWithEmails)

    // @ts-ignore
    deleteRestockSubscriptionStep(restockedSubscriptionsWithEmails)

    return new WorkflowResponse({
      subscriptions: restockedSubscriptionsWithEmails,
    })
  }
)
```

This workflow has the following steps:

1. `getDistinctSubscriptionsStep` to retrieve the restock subscriptions by distinct variant and sales channel ID pairings.
2. `getRestockedStep` to filter the subscriptions retrieved by the previous step and return only those whose variants have been restocked.
3. `useQueryGraphStep` to retrieve all subscriptions that have a restocked variant and sales channel ID pairing using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). Notice that in the specified `fields` you pass `product_variant.*`, which retrieves the details of the subscription's variant from the Product Module. This is possible due to the module link you created between the `RestockSubscription` and `ProductVariant` models in an earlier step.
4. `sendRestockNotificationStep` to send the notification to the subscribers of the restocked variants.
5. `deleteRestockSubscriptionStep` to delete the restock subscriptions since their subscribers have been notified.

The workflow returns the restocked subscriptions, which are now deleted.

You'll execute this workflow in the next section.

***

## Step 7: Send Restock Notifications Daily

Now that you've built the flow to send restock notifications, you want to check for restocked variants and send notifications to their subscribers once a day. To do so, you'll use a scheduled job.

A scheduled job is an asynchronous function that the Medusa application runs at the schedule you specify during the Medusa application's runtime. Scheduled jobs are useful for automating tasks at a fixed schedule.

Learn more about scheduled jobs in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md).

In this step, you'll create a scheduled job that runs once a day to execute the `sendRestockNotificationsWorkflow` from the previous step.

A scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory. So, create the file `src/jobs/check-restock.ts` with the following content:

![The directory structure of the Medusa application after adding the scheduled job.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733234524/Medusa%20Resources/restock-dir-overview-21_reaqp3.jpg)

```ts title="src/jobs/check-restock.ts"
import {
  MedusaContainer,
} from "@medusajs/framework/types"
import { 
  sendRestockNotificationsWorkflow,
} from "../workflows/send-restock-notifications"

export default async function myCustomJob(container: MedusaContainer) {
  await sendRestockNotificationsWorkflow(container)
    .run()
}

export const config = {
  name: "check-restock",
  schedule: "0 0 * * *", // For debugging, change to `* * * * *`
}
```

In this file, you export:

- An asynchronous function, which is the task to execute at the specified schedule.
- A configuration object having the following properties:
  - `name`: A unique name for the scheduled job.
  - `schedule`: A [cron expression](https://crontab.guru/) string indicating the schedule to run the job at. The specified schedule indicates that this job should run every day at midnight.

The scheduled job function accepts the Medusa container as a parameter. In the function, you execute the `sendRestockNotificationsWorkflow` by invoking it, passing it the container, then executing its `run` method.

### Test Scheduled Job

To test out the scheduled job, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin again at `http://localhost:9000/app` and log in. After that:

1. Go to the same product -> variant that you edited earlier to make out of stock.
2. On the variant's details page, click on an inventory item under the Inventory Items section.

![The inventory items table shows the variant's items. Click on an item to open its details page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733230649/Medusa%20Resources/Screenshot_2024-12-03_at_2.57.01_PM_ccc9of.png)

3. On the inventory item's page, click on the three dots icon next to a location, then choose edit from the dropdown.

![The locations are shown in a table. Click on the three-dots at a location's right side, then choose Edit from the dropdown.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733230730/Medusa%20Resources/Screenshot_2024-12-03_at_2.58.18_PM_waeepw.png)

4. In the drawer form, enter any value greater than `0`.
5. Click the Save button.

![In the drawer form, enter value greater than 0 in the In stock field, then click the Save button at the bottom.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733234710/Medusa%20Resources/Screenshot_2024-12-03_at_3.53.55_PM_uwt06f.png)

With this change, the variant you previously subscribed to is now restocked. To trigger the scheduled job to run, change its `config` object to run every minute:

```ts title="src/jobs/check-restock.ts"
// ...
export const config = {
  // ...
  schedule: "* * * * *", // For debugging, change to `* * * * *`
}
```

After the application restarts, wait for the scheduled job to execute. If you're using the default Notification Module Provider that logs notifications in the terminal, you'll see a message similar to the following:

```bash
Attempting to send a notification to: 'customer@gmail.com' on the channel: 'email' with template: 'variant-restock' and data: '{"variant":{"id":"variant_01JE3H6WHFMJ2WS64RM2MV1CJ6",...}}'
```

***

## Next Steps

You've now implemented restock notifications in Medusa. You can also customize the [storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to allow customers to subscribe to the restock notification using the new API route you added.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Digital Products Recipe Example

In this guide, you'll learn how to support digital products in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with support for customizations. Medusa provides all features related to products and managing them, and the Medusa Framework allows you to extend those features and implement your custom use case.

You can extend Medusa's product features to support selling, storing, and fulfilling digital products. In this guide, you'll customize Medusa to add the following features:

1. Support digital products with multiple media items.
2. Manage digital products from the admin dashboard.
3. Handle and fulfill digital product orders.
4. Allow customers to download their digital product purchases from the storefront.
5. All other commerce features that Medusa provides.

This guide provides an example of an approach to implement digital products. You're free to choose a different approach using the Medusa Framework.

- [Digital Products Example Repository](https://github.com/medusajs/examples/tree/main/digital-product): Find the full code for this recipe example in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1721654620/OpenApi/Digital_Products_Postman_vjr3jg.yml): Imported this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credential and submit the form. Afterwards, you can login with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create the Digital Product Module

Medusa creates commerce features in modules. For example, product features and data models are created in the Product Module.

You also create custom commerce data models and features in custom modules. They're integrated into the Medusa application similar to Medusa's modules without side effects.

So, you'll create a digital product module that holds the data models related to a digital product and allows you to manage them.

Create the directory `src/modules/digital-product`.

### Create Data Models

Create the file `src/modules/digital-product/models/digital-product.ts` with the following content:

```ts title="src/modules/digital-product/models/digital-product.ts"
import { model } from "@medusajs/framework/utils"
import DigitalProductMedia from "./digital-product-media"
import DigitalProductOrder from "./digital-product-order"

const DigitalProduct = model.define("digital_product", {
  id: model.id().primaryKey(),
  name: model.text(),
  medias: model.hasMany(() => DigitalProductMedia, {
    mappedBy: "digitalProduct",
  }),
  orders: model.manyToMany(() => DigitalProductOrder, {
    mappedBy: "products",
  }),
})
.cascades({
  delete: ["medias"],
})

export default DigitalProduct
```

This creates a `DigitalProduct` data model. It has many medias and orders, which you’ll create next.

Create the file `src/modules/digital-product/models/digital-product-media.ts` with the following content:

```ts title="src/modules/digital-product/models/digital-product-media.ts" highlights={dpmModelHighlights}
import { model } from "@medusajs/framework/utils"
import { MediaType } from "../types"
import DigitalProduct from "./digital-product"

const DigitalProductMedia = model.define("digital_product_media", {
  id: model.id().primaryKey(),
  type: model.enum(MediaType),
  fileId: model.text(),
  mimeType: model.text(),
  digitalProduct: model.belongsTo(() => DigitalProduct, {
    mappedBy: "medias",
  }),
})

export default DigitalProductMedia
```

This creates a `DigitalProductMedia` data model, which represents a media file that belongs to the digital product. The `fileId` property holds the ID of the uploaded file as returned by the File Module, which is explained in later sections.

Notice that the above data model uses an enum from a `types` file. So, create the file `src/modules/digital-product/types/index.ts` with the following content:

```ts title="src/modules/digital-product/types/index.ts"
export enum MediaType {
  MAIN = "main",
  PREVIEW = "preview"
}
```

This enum indicates that a digital product media can either be used to preview the digital product, or is the main file available on purchase.

Next, create the file `src/modules/digital-product/models/digital-product-order.ts` with the following content:

```ts title="src/modules/digital-product/models/digital-product-order.ts"
import { model } from "@medusajs/framework/utils"
import { OrderStatus } from "../types"
import DigitalProduct from "./digital-product"

const DigitalProductOrder = model.define("digital_product_order", {
  id: model.id().primaryKey(),
  status: model.enum(OrderStatus),
  products: model.manyToMany(() => DigitalProduct, {
    mappedBy: "orders",
    pivotTable: "digitalproduct_digitalproductorders",
  }),
})

export default DigitalProductOrder
```

This creates a `DigitalProductOrder` data model, which represents an order of digital products.

This data model also uses an enum from the `types` file. So, add the following to the `src/modules/digital-product/types/index.ts` file:

```ts title="src/modules/digital-product/types/index.ts"
export enum OrderStatus {
  PENDING = "pending",
  SENT = "sent"
}
```

### Create Main Module Service

Next, create the main service of the module at `src/modules/digital-product/service.ts` with the following content:

```ts title="src/modules/digital-product/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import DigitalProduct from "./models/digital-product"
import DigitalProductOrder from "./models/digital-product-order"
import DigitalProductMedia from "./models/digital-product-media"

class DigitalProductModuleService extends MedusaService({
  DigitalProduct,
  DigitalProductMedia,
  DigitalProductOrder,
}) {

}

export default DigitalProductModuleService
```

The service extends the [service factory](https://docs.medusajs.com/docs/learn/fundamentals/modules/service-factory/index.html.md), which provides basic data-management features.

### Create Module Definition

After that, create the module definition at `src/modules/digital-product/index.ts` with the following content:

```ts title="src/modules/digital-product/index.ts"
import DigitalProductModuleService from "./service"
import { Module } from "@medusajs/framework/utils"

export const DIGITAL_PRODUCT_MODULE = "digitalProductModuleService"

export default Module(DIGITAL_PRODUCT_MODULE, {
  service: DigitalProductModuleService,
})
```

### Add Module to Medusa Configuration

Finally, add the module to the list of modules in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/digital-product",
    },
  ],
})
```

### Further Reads

- [How to Create a Module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md)
- [How to Create a Data Model](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md)

***

## Step 3: Define Links

Modules are isolated in Medusa, making them reusable, replaceable, and integrable in your application without side effects.

So, you can't have relations between data models in modules. Instead, you define a link between them.

Links are relations between data models of different modules that maintain the isolation between the modules.

In this step, you’ll define links between your module’s data models and data models from Medusa’s Commerce Modules:

1. Link between the `DigitalProduct` model and the Product Module's `ProductVariant` model.
2. Link between the `DigitalProductOrder` model and the Order Module's `Order` model.

Start by creating the file `src/links/digital-product-variant.ts` with the following content:

```ts title="src/links/digital-product-variant.ts"
import DigitalProductModule from "../modules/digital-product"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: DigitalProductModule.linkable.digitalProduct,
    deleteCascade: true,
  },
  ProductModule.linkable.productVariant
)

```

This defines a link between `DigitalProduct` and the Product Module’s `ProductVariant`. This allows product variants that customers purchase to be digital products.

`deleteCascades` is enabled on the `digitalProduct` so that when a product variant is deleted, its linked digital product is also deleted.

Next, create the file `src/links/digital-product-order.ts` with the following content:

```ts title="src/links/digital-product-order.ts"
import DigitalProductModule from "../modules/digital-product"
import OrderModule from "@medusajs/medusa/order"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: DigitalProductModule.linkable.digitalProductOrder,
    deleteCascade: true,
  },
  OrderModule.linkable.order
)

```

This defines a link between `DigitalProductOrder` and the Order Module’s `Order`. This keeps track of orders that include purchases of digital products.

`deleteCascades` is enabled on the `digitalProductOrder` so that when a Medusa order is deleted, its linked digital product order is also deleted.

### Further Read

- [How to Define Module Links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md)

***

## Step 4: Run Migrations and Sync Links

To create tables for the digital product data models in the database, start by generating the migrations for the Digital Product Module with the following command:

```bash
npx medusa db:generate digitalProductModuleService
```

This generates a migration in the `src/modules/digital-product/migrations` directory.

Then, reflect the migrations and links in the database with the following command:

```bash
npx medusa db:migrate
```

***

## Step 5: List Digital Products Admin API Route

To expose custom commerce features to frontend applications, such as the Medusa Admin dashboard or a storefront, you expose an endpoint by creating an API route.

In this step, you’ll create the admin API route to list digital products.

Create the file `src/api/admin/digital-products/route.ts` with the following content:

```ts title="src/api/admin/digital-products/route.ts"
import { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const { 
    fields, 
    limit = 20, 
    offset = 0,
  } = req.validatedQuery || {}
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { 
    data: digitalProducts,
    metadata: { count, take, skip } = {},
  } = await query.graph({
    entity: "digital_product",
    fields: [
      "*",
      "medias.*",
      "product_variant.*",
      ...(fields || []),
    ],
    pagination: {
      skip: offset,
      take: limit,
    },
  })

  res.json({
    digital_products: digitalProducts,
    count,
    limit: take,
    offset: skip,
  })
}
```

This adds a `GET` API route at `/admin/digital-products`.

In the route handler, you use Query to retrieve the list of digital products and their relations. The route handler also supports pagination.

### Test API Route

To test out the API route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, obtain a JWT token as an admin user with the following request:

```bash
curl -X POST 'http://localhost:9000/auth/user/emailpass' \
-H 'Content-Type: application/json' \
--data-raw '{
    "email": "admin@medusajs.com",
    "password": "supersecret"
}'
```

Finally, send the following request to retrieve the list of digital products:

```bash
curl -L 'http://localhost:9000/admin/digital-products' \
-H 'Authorization: Bearer {token}'
```

Make sure to replace `{token}` with the JWT token you retrieved.

### Further Reads

- [How to Create an API Route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md)
- [Learn more about Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md)

***

## Step 6: Create Digital Product Workflow

To implement and expose a feature that manipulates data, you create a workflow that uses services to implement the functionality, then create an API route that executes that workflow.

In this step, you’ll create a workflow that creates a digital product. You’ll use this workflow in an API route in the next section.

This workflow has the following steps:

```mermaid
graph TD
  createProductsWorkflow["createProductsWorkflow (Medusa)"] --> createDigitalProductStep
  createDigitalProductStep --> createDigitalProductMediasStep
  createDigitalProductMediasStep --> createRemoteLinkStep["createRemoteLinkStep (Medusa)"]
```

1. `createProductsWorkflow`: Create the Medusa product that the digital product is associated with its variant. Medusa provides this workflow through the `@medusajs/medusa/core-flows` package, which you can use as a step.
2. `createDigitalProductStep`: Create the digital product.
3. `createDigitalProductMediasStep`: Create the medias associated with the digital product.
4. `createRemoteLinkStep`: Create the link between the digital product and the product variant. Medusa provides this step through the `@medusajs/medusa/core-flows` package.

You’ll implement the second and third steps.

### createDigitalProductStep (Second Step)

Create the file `src/workflows/create-digital-product/steps/create-digital-product.ts` with the following content:

```ts title="src/workflows/create-digital-product/steps/create-digital-product.ts" highlights={createDpHighlights} collapsibleLines="1-7" expandMoreLabel="Show Imports"
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import DigitalProductModuleService from "../../../modules/digital-product/service"
import { DIGITAL_PRODUCT_MODULE } from "../../../modules/digital-product"

export type CreateDigitalProductStepInput = {
  name: string
}

const createDigitalProductStep = createStep(
  "create-digital-product-step",
  async (data: CreateDigitalProductStepInput, { container }) => {
    const digitalProductModuleService: DigitalProductModuleService = 
      container.resolve(DIGITAL_PRODUCT_MODULE)

    const digitalProduct = await digitalProductModuleService
      .createDigitalProducts(data)
    
    return new StepResponse({
      digital_product: digitalProduct,
    }, {
      digital_product: digitalProduct,
    })
  },
  async (data, { container }) => {
    if (!data) {
      return
    }
    const digitalProductModuleService: DigitalProductModuleService = 
      container.resolve(DIGITAL_PRODUCT_MODULE)
    
    await digitalProductModuleService.deleteDigitalProducts(
      data.digital_product.id
    )
  }
)

export default createDigitalProductStep
```

This creates the `createDigitalProductStep`. In this step, you create a digital product.

In the compensation function, which is executed if an error occurs in the workflow, you delete the digital products.

### createDigitalProductMediasStep (Third Step)

Create the file `src/workflows/create-digital-product/steps/create-digital-product-medias.ts` with the following content:

```ts title="src/workflows/create-digital-product/steps/create-digital-product-medias.ts" highlights={createDigitalProductMediaHighlights} collapsibleLines="1-8" expandMoreLabel="Show Imports"
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import DigitalProductModuleService from "../../../modules/digital-product/service"
import { DIGITAL_PRODUCT_MODULE } from "../../../modules/digital-product"
import { MediaType } from "../../../modules/digital-product/types"

export type CreateDigitalProductMediaInput = {
  type: MediaType
  fileId: string
  mimeType: string
  digital_product_id: string
}

type CreateDigitalProductMediasStepInput = {
  medias: CreateDigitalProductMediaInput[]
}

const createDigitalProductMediasStep = createStep(
  "create-digital-product-medias",
  async ({ 
    medias,
  }: CreateDigitalProductMediasStepInput, { container }) => {
    const digitalProductModuleService: DigitalProductModuleService = 
      container.resolve(DIGITAL_PRODUCT_MODULE)

    const digitalProductMedias = await digitalProductModuleService
      .createDigitalProductMedias(medias)

    return new StepResponse({
      digital_product_medias: digitalProductMedias,
    }, {
      digital_product_medias: digitalProductMedias,
    })
  },
  async (data, { container }) => {
    if (!data) {
      return
    }
    const digitalProductModuleService: DigitalProductModuleService = 
      container.resolve(DIGITAL_PRODUCT_MODULE)
    
    await digitalProductModuleService.deleteDigitalProductMedias(
      data.digital_product_medias.map((media) => media.id)
    )
  }
)

export default createDigitalProductMediasStep
```

This creates the `createDigitalProductMediasStep`. In this step, you create medias of the digital product.

In the compensation function, you delete the digital product medias.

### Create createDigitalProductWorkflow

Finally, create the file `src/workflows/create-digital-product/index.ts` with the following content:

```ts title="src/workflows/create-digital-product/index.ts" highlights={createDpWorkflowHighlights} collapsibleLines="1-23" expandMoreLabel="Show Imports"
import { 
  createWorkflow,
  transform,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
  CreateProductWorkflowInputDTO,
} from "@medusajs/framework/types"
import { 
  createProductsWorkflow,
  createRemoteLinkStep,
} from "@medusajs/medusa/core-flows"
import { 
  Modules,
} from "@medusajs/framework/utils"
import createDigitalProductStep, { 
  CreateDigitalProductStepInput,
} from "./steps/create-digital-product"
import createDigitalProductMediasStep, { 
  CreateDigitalProductMediaInput,
} from "./steps/create-digital-product-medias"
import { DIGITAL_PRODUCT_MODULE } from "../../modules/digital-product"

type CreateDigitalProductWorkflowInput = {
  digital_product: CreateDigitalProductStepInput & {
    medias: Omit<CreateDigitalProductMediaInput, "digital_product_id">[]
  }
  product: CreateProductWorkflowInputDTO
}

const createDigitalProductWorkflow = createWorkflow(
  "create-digital-product",
  (input: CreateDigitalProductWorkflowInput) => {
    const { medias, ...digitalProductData } = input.digital_product

    const product = createProductsWorkflow.runAsStep({
      input: {
        products: [input.product],
      },
    })

    const { digital_product } = createDigitalProductStep(
      digitalProductData
    )

    const { digital_product_medias } = createDigitalProductMediasStep(
      transform({
        digital_product,
        medias,
      },
      (data) => ({
        medias: data.medias.map((media) => ({
          ...media,
          digital_product_id: data.digital_product.id,
        })),
      })
      )
    )

    createRemoteLinkStep([{
      [DIGITAL_PRODUCT_MODULE]: {
        digital_product_id: digital_product.id,
      },
      [Modules.PRODUCT]: {
        product_variant_id: product[0].variants[0].id,
      },
    }])

    return new WorkflowResponse({
      digital_product: {
        ...digital_product,
        medias: digital_product_medias,
      },
    })
  }
)

export default createDigitalProductWorkflow
```

This creates the `createDigitalProductWorkflow`. The workflow accepts as a parameter the digital product and the Medusa product to create.

In the workflow, you run the following steps:

1. `createProductsWorkflow` as a step to create a Medusa product.
2. `createDigitalProductStep` to create the digital product.
3. `createDigitalProductMediasStep` to create the digital product’s medias.
4. `createRemoteLinkStep` to link the digital product to the product variant.

You’ll test out the workflow in the next section.

### Further Reads

- [How to Create a Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md)
- [What is the Compensation Function](https://docs.medusajs.com/docs/learn/fundamentals/workflows/compensation-function/index.html.md)
- [Learn more about Link functions](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md)

***

## Step 7: Create Digital Product API Route

In this step, you’ll add the API route to create a digital product using the `createDigitalProductWorkflow`.

In the file `src/api/admin/digital-products/route.ts` add a new route handler:

```ts title="src/api/admin/digital-products/route.ts"
// other imports...
import { z } from "zod"
import createDigitalProductWorkflow from "../../../workflows/create-digital-product"
import { CreateDigitalProductMediaInput } from "../../../workflows/create-digital-product/steps/create-digital-product-medias"
import { createDigitalProductsSchema } from "../../validation-schemas"

// ...

type CreateRequestBody = z.infer<
  typeof createDigitalProductsSchema
>

export const POST = async (
  req: AuthenticatedMedusaRequest<CreateRequestBody>,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
  const { data: [shippingProfile] } = await query.graph({
    entity: "shipping_profile",
    fields: ["id"],
  })

  const { result } = await createDigitalProductWorkflow(
    req.scope
  ).run({
    input: {
      digital_product: {
        name: req.validatedBody.name,
        medias: req.validatedBody.medias.map((media) => ({
          fileId: media.file_id,
          mimeType: media.mime_type,
          ...media,
        })) as Omit<CreateDigitalProductMediaInput, "digital_product_id">[],
      },
      product: {
        ...req.validatedBody.product,
        shipping_profile_id: shippingProfile.id,
      },
    },
  })

  res.json({
    digital_product: result.digital_product,
  })
}
```

This adds a `POST` API route at `/admin/digital-products`. In the route handler, you first retrieve a shipping profile to associate it with the product, which is required.

Then, you execute the `createDigitalProductWorkflow` created in the previous step, passing data from the request body as input, along with the retrieved shipping profile.

The route handler imports a validation schema from a `validation-schema` file. So, create the file `src/api/validation-schemas.ts` with the following content:

```ts title="src/api/validation-schemas.ts"
import { 
  AdminCreateProduct,
} from "@medusajs/medusa/api/admin/products/validators"
import { z } from "zod"
import { MediaType } from "../modules/digital-product/types"

export const createDigitalProductsSchema = z.object({
  name: z.string(),
  medias: z.array(z.object({
    type: z.nativeEnum(MediaType),
    file_id: z.string(),
    mime_type: z.string(),
  })),
  product: AdminCreateProduct(),
})
```

This defines the expected request body schema.

Finally, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { createDigitalProductsSchema } from "./validation-schemas"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/admin/digital-products",
      method: "POST",
      middlewares: [
        validateAndTransformBody(createDigitalProductsSchema),
      ],
    },
  ],
})
```

This adds a validation middleware to ensure that the body of `POST` requests sent to `/admin/digital-products` match the `createDigitalProductsSchema`.

### Further Read

- [How to Create a Middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md)

***

## Step 8: Upload Digital Product Media API Route

To upload the digital product media files, use Medusa’s File Module.

Your Medusa application uses the local file module provider by default, which uploads files to a local directory. However, you can use other file module providers, such as the [S3 module provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/s3/index.html.md).

In this step, you’ll create an API route for uploading preview and main digital product media files.

Before creating the API route, install the [multer express middleware](https://expressjs.com/en/resources/middleware/multer.html) to support file uploads:

```bash npm2yarn
npm install multer
npm install --save-dev @types/multer
```

Then, create the file `src/api/admin/digital-products/upload/[type]/route.ts` with the following content:

```ts title="src/api/admin/digital-products/upload/[type]/route.ts" highlights={uploadHighlights} collapsibleLines="1-7" expandMoreLabel="Show Imports"
import {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { uploadFilesWorkflow } from "@medusajs/medusa/core-flows"
import { MedusaError } from "@medusajs/framework/utils"

export const POST = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const access = req.params.type === "main" ? "private" : "public"
  const input = req.files as Express.Multer.File[]

  if (!input?.length) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "No files were uploaded"
    )
  }

  const { result } = await uploadFilesWorkflow(req.scope).run({
    input: {
      files: input?.map((f) => ({
        filename: f.originalname,
        mimeType: f.mimetype,
        content: f.buffer.toString("base64"),
        access,
      })),
    },
  })

  res.status(200).json({ files: result })
}

```

This adds a `POST` API route at `/admin/digital-products/upload/[type]` where `[type]` is either `preview` or `main`.

In the route handler, you use `uploadFilesWorkflow` from Medusa's core workflows to upload the file. If the file type is `main`, it’s uploaded with private access, as only customers who purchased it can download it. Otherwise, it’s uploaded with `public` access.

Next, add to the file `src/api/middlewares.ts` the `multer` middleware on this API route:

```ts title="src/api/middlewares.ts"
// other imports...
import multer from "multer"

const upload = multer({ storage: multer.memoryStorage() })

export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/digital-products/upload**",
      method: "POST",
      middlewares: [
        upload.array("files"),
      ],
    },
  ],
})
```

You’ll test out this API route in the next step as you use these API routes in the admin customizations.

***

## Step 9: Add Digital Products UI Route in Admin

The Medusa Admin is customizable, allowing you to inject widgets into existing pages or add UI routes to create new pages.

In this step, you’ll add a UI route to the Medusa Admin that displays a list of digital products.

Before you create the UI route, create the file `src/admin/types/index.ts` that holds the following types:

```ts title="src/admin/types/index.ts"
import { ProductVariantDTO } from "@medusajs/framework/types"

export enum MediaType {
  MAIN = "main",
  PREVIEW = "preview"
}

export type DigitalProductMedia = {
  id: string
  type: MediaType
  fileId: string
  mimeType: string
  digitalProducts?: DigitalProduct
}

export type DigitalProduct = {
  id: string
  name: string
  medias?: DigitalProductMedia[]
  product_variant?:ProductVariantDTO
}

```

These types will be used by the UI route.

Next, create the file `src/admin/routes/digital-products/page.tsx` with the following content:

```tsx title="src/admin/routes/digital-products/page.tsx" highlights={digitalProductPageHighlights} collapsibleLines="1-7" expandMoreLabel="Show Imports"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { PhotoSolid } from "@medusajs/icons"
import { Container, Heading, Table } from "@medusajs/ui"
import { useState } from "react"
import { Link } from "react-router-dom"
import { DigitalProduct } from "../../types"

const DigitalProductsPage = () => {
  const [digitalProducts, setDigitalProducts] = useState<
    DigitalProduct[]
  >([])
  // TODO fetch digital products...

  return (
    <Container>
      <div className="flex justify-between items-center mb-4">
        <Heading level="h2">Digital Products</Heading>
        {/* TODO add create button */}
      </div>
      <Table>
        <Table.Header>
          <Table.Row>
            <Table.HeaderCell>Name</Table.HeaderCell>
            <Table.HeaderCell>Action</Table.HeaderCell>
          </Table.Row>
        </Table.Header>
        <Table.Body>
          {digitalProducts.map((digitalProduct) => (
            <Table.Row key={digitalProduct.id}>
              <Table.Cell>
                {digitalProduct.name}
              </Table.Cell>
              <Table.Cell>
                <Link to={`/products/${digitalProduct.product_variant?.product_id}`}>
                  View Product
                </Link>
              </Table.Cell>
            </Table.Row>
          ))}
        </Table.Body>
      </Table>
      {/* TODO add pagination component */}
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Digital Products",
  icon: PhotoSolid,
})

export default DigitalProductsPage

```

This creates a UI route that's displayed at the `/digital-products` path in the Medusa Admin. The UI route also adds a sidebar item with the label “Digital Products" pointing to the UI route.

In the React component of the UI route, you just display the table of digital products.

Next, replace the first `TODO` with the following:

```tsx title="src/admin/routes/digital-products/page.tsx" highlights={paginationHighlights}
// other imports...
import { useMemo } from "react"

const DigitalProductsPage = () => {
  // ...
    
  const [currentPage, setCurrentPage] = useState(0)
  const pageLimit = 20
  const [count, setCount] = useState(0)
  const pagesCount = useMemo(() => {
    return count / pageLimit
  }, [count])
  const canNextPage = useMemo(
    () => currentPage < pagesCount - 1, 
    [currentPage, pagesCount]
  )
  const canPreviousPage = useMemo(
    () => currentPage > 0, 
    [currentPage]
  )

  const nextPage = () => {
    if (canNextPage) {
      setCurrentPage((prev) => prev + 1)
    }
  }

  const previousPage = () => {
    if (canPreviousPage) {
      setCurrentPage((prev) => prev - 1)
    }
  }
  
  // TODO fetch digital products
    
  // ...
}
```

This defines the following pagination variables:

1. `currentPage`: The number of the current page.
2. `pageLimit`: The number of digital products to show per page.
3. `count`: The total count of digital products.
4. `pagesCount`: A memoized variable that holds the number of pages based on `count` and `pageLimit`.
5. `canNextPage`: A memoized variable that indicates whether there’s a next page based on whether the current page is less than `pagesCount - 1`.
6. `canPreviousPage`: A memoized variable that indicates whether there’s a previous pages based on whether the current page is greater than `0`.
7. `nextPage`: A function that increments the `currentPage`.
8. `previousPage`: A function that decrements the `currentPage`.

Then, replace the new `TODO fetch digital products` with the following:

```tsx title="src/admin/routes/digital-products/page.tsx" highlights={fetchDigitalProductsHighlights}
// other imports
import { useEffect } from "react"

const DigitalProductsPage = () => {
  // ...
    
  const fetchProducts = () => {
    const query = new URLSearchParams({
      limit: `${pageLimit}`,
      offset: `${pageLimit * currentPage}`,
    })
    
    fetch(`/admin/digital-products?${query.toString()}`, {
      credentials: "include",
    })
    .then((res) => res.json())
    .then(({ 
      digital_products: data, 
      count,
    }) => {
      setDigitalProducts(data)
      setCount(count)
    })
  }

  useEffect(() => {
    fetchProducts()
  }, [currentPage])
    
  // ...
}
```

This defines a `fetchProducts` function that fetches the digital products using the API route you created in step 4. You also call that function within a `useEffect` callback which is executed whenever the `currentPage` changes.

Finally, replace the `TODO add pagination component` in the return statement with `Table.Pagination` component:

```tsx title="src/admin/routes/digital-products/page.tsx"
return (
    <Container>
      {/* ... */}
      <Table.Pagination
        count={count}
        pageSize={pageLimit}
        pageIndex={currentPage}
        pageCount={pagesCount}
        canPreviousPage={canPreviousPage}
        canNextPage={canNextPage}
        previousPage={previousPage}
        nextPage={nextPage}
      />
    </Container>
  )
```

The `Table.Pagination` component accepts as props the pagination variables you defined earlier.

### Test UI Route

To test the UI route out, start the Medusa application, go to `localhost:9000/app`, and log in as an admin user.

Once you log in, you’ll find a new sidebar item, “Digital Products.” If you click on it, you’ll see the UI route you created with a table of digital products.

### Further Reads

- [How to Create UI Routes](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md)
- [How to Create Admin Widgets](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md)

***

## Step 10: Add Create Digital Product Form in Admin

In this step, you’ll add a form for admins to create digital products. The form opens in a drawer or side window from within the Digital Products UI route you created in the previous section.

Create the file `src/admin/components/create-digital-product-form/index.tsx` with the following content:

```tsx title="src/admin/components/create-digital-product-form/index.tsx"
import { useState } from "react"
import { Input, Button, Select, toast } from "@medusajs/ui"
import { MediaType } from "../../types"

type CreateMedia = {
  type: MediaType
  file?: File
}

type Props = {
  onSuccess?: () => void
}

const CreateDigitalProductForm = ({
  onSuccess,
}: Props) => {
  const [name, setName] = useState("")
  const [medias, setMedias] = useState<CreateMedia[]>([])
  const [productTitle, setProductTitle] = useState("")
  const [loading, setLoading] = useState(false)

  const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    // TODO handle submit
  }

  return (
    <form onSubmit={onSubmit}>
      {/* TODO show form inputs */}
      <Button 
        type="submit"
        isLoading={loading}
      >
        Create
      </Button>
    </form>
  )
}

export default CreateDigitalProductForm
```

This creates a React component that shows a form and handles creating a digital product on form submission.

You currently don’t display the form. Replace the return statement with the following:

```tsx title="src/admin/components/create-digital-product-form/index.tsx"
return (
  <form onSubmit={onSubmit}>
    <Input
      name="name"
      placeholder="Name"
      type="text"
      value={name}
      onChange={(e) => setName(e.target.value)}
    />
    <fieldset className="my-4">
      <legend className="mb-2">Media</legend>
      <Button type="button" onClick={onAddMedia}>Add Media</Button>
      {medias.map((media, index) => (
        <fieldset className="my-2 p-2 border-solid border rounded">
          <legend>Media {index + 1}</legend>
          <Select 
            value={media.type} 
            onValueChange={(value) => changeFiles(
              index,
              {
                type: value as MediaType,
              }
            )}
          >
            <Select.Trigger>
              <Select.Value placeholder="Media Type" />
            </Select.Trigger>
            <Select.Content>
              <Select.Item value={MediaType.PREVIEW}>
                Preview
              </Select.Item>
              <Select.Item value={MediaType.MAIN}>
                Main
              </Select.Item>
            </Select.Content>
          </Select>
          <Input
            name={`file-${index}`}
            type="file"
            onChange={(e) => changeFiles(
              index,
              {
                file: e.target.files?.[0],
              }
            )}
            className="mt-2"
          />
        </fieldset>
      ))}
    </fieldset>
    <fieldset className="my-4">
      <legend className="mb-2">Product</legend>
      <Input
        name="product_title"
        placeholder="Product Title"
        type="text"
        value={productTitle}
        onChange={(e) => setProductTitle(e.target.value)}
      />
    </fieldset>
    <Button 
      type="submit"
      isLoading={loading}
    >
      Create
    </Button>
  </form>
)
```

This shows input fields for the digital product and product’s names. It also shows a fieldset of media files, with the ability to add more media files on a button click.

Add in the component the `onAddMedia` function that is triggered by a button click to add a new media:

```tsx title="src/admin/components/create-digital-product-form/index.tsx"
const onAddMedia = () => {
  setMedias((prev) => [
    ...prev,
    {
      type: MediaType.PREVIEW,
    },
  ])
}
```

And add in the component a `changeFiles` function that saves changes related to a media in the `medias` state variable:

```tsx title="src/admin/components/create-digital-product-form/index.tsx"
const changeFiles = (
  index: number,
  data: Partial<CreateMedia>
) => {
  setMedias((prev) => [
    ...(prev.slice(0, index)),
    {
      ...prev[index],
      ...data,
    },
    ...(prev.slice(index + 1)),
  ])
}
```

On submission, the media files should first be uploaded before the digital product is created.

So, add before the `onSubmit` function the following new function:

```tsx title="src/admin/components/create-digital-product-form/index.tsx"
const uploadMediaFiles = async (
  type: MediaType
) => {
  const formData = new FormData()
  const mediaWithFiles = medias.filter(
    (media) => media.file !== undefined && 
      media.type === type
  )

  if (!mediaWithFiles.length) {
    return
  }

  mediaWithFiles.forEach((media) => {
      if (!media.file) {
        return
      }
    formData.append("files", media.file)
  })

  const { files } = await fetch(`/admin/digital-products/upload/${type}`, {
    method: "POST",
    credentials: "include",
    body: formData,
  }).then((res) => res.json())

  return {
    mediaWithFiles,
    files,
  }
}
```

This function accepts a type of media to upload (`preview` or `main`). In the function, you upload the files of the specified type using the API route you created in step 7. You return the uploaded files and their associated media.

Next, you’ll implement the `onSubmit` function. Replace it with the following:

```tsx title="src/admin/components/create-digital-product-form/index.tsx" highlights={uploadMediaHighlights}
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault()
  setLoading(true)

  try {
    const {
      mediaWithFiles: previewMedias,
      files: previewFiles,
    } = await uploadMediaFiles(MediaType.PREVIEW) || {}
    const {
      mediaWithFiles: mainMedias,
      files: mainFiles,
    } = await uploadMediaFiles(MediaType.MAIN) || {}

    const mediaData: {
        type: MediaType
        file_id: string
        mime_type: string
      }[] = []

    previewMedias?.forEach((media, index) => {
      mediaData.push({
        type: media.type,
        file_id: previewFiles[index].id,
        mime_type: media.file!.type,
      })
    })

    mainMedias?.forEach((media, index) => {
      mediaData.push({
        type: media.type,
        file_id: mainFiles[index].id,
        mime_type: media.file!.type,
      })
    })

    // TODO create digital product
  } catch (e) {
    console.error(e)
    setLoading(false)
  }
}
```

In this function, you use the `uploadMediaFiles` function to upload `preview` and `main` media files. Then, you prepare the media data that’ll be used when creating the digital product in a `mediaData` variable.

Notice that you use the `id` of uploaded files, as returned in the response of `/admin/digital-products/upload/[type]` as the `file_id` value of the media to be created.

Finally, replace the new `TODO` in `onSubmit` with the following:

```tsx title="src/admin/components/create-digital-product-form/index.tsx"
fetch(`/admin/digital-products`, {
  method: "POST",
  credentials: "include",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name,
    medias: mediaData,
    product: {
      title: productTitle,
      options: [{
        title: "Default",
        values: ["default"],
      }],
      variants: [{
        title: productTitle,
        options: {
          Default: "default",
        },
        manage_inventory: false,
        // delegate setting the prices to the
        // product's page.
        prices: [],
        shipping_profile_id: "",
      }],
    },
  }),
})
.then((res) => res.json())
.then(({ message }) => {
  if (message) {
    throw message
  }
  onSuccess?.()
})
.catch((e) => {
  console.error(e)
  toast.error("Error", {
    description: `An error occurred while creating the digital product: ${e}`,
  })
})
.finally(() => setLoading(false))
```

In this snippet, you send a `POST` request to `/admin/digital-products` to create a digital product.

You’ll make changes now to `src/admin/routes/digital-products/page.tsx` to show the form.

First, add a new `open` state variable:

```tsx title="src/admin/routes/digital-products/page.tsx"
const DigitalProductsPage = () => {
  const [open, setOpen] = useState(false)
  // ...
}
```

Then, replace the `TODO add create button` in the return statement to show the `CreateDigitalProductForm` component:

```tsx title="src/admin/routes/digital-products/page.tsx"
// other imports...
import { Drawer } from "@medusajs/ui"
import CreateDigitalProductForm from "../../components/create-digital-product-form"

const DigitalProductsPage = () => {
  // ...
    
  return (
    <Container>
      {/* Replace the TODO with the following */}
      <Drawer open={open} onOpenChange={(openChanged) => setOpen(openChanged)}>
        <Drawer.Trigger 
          onClick={() => {
            setOpen(true)
          }}
          asChild
        >
          <Button>Create</Button>
        </Drawer.Trigger>
        <Drawer.Content>
          <Drawer.Header>
            <Drawer.Title>Create Product</Drawer.Title>
          </Drawer.Header>
          <Drawer.Body>
            <CreateDigitalProductForm onSuccess={() => {
              setOpen(false)
              if (currentPage === 0) {
                fetchProducts()
              } else {
                setCurrentPage(0)
              }
            }} />
          </Drawer.Body>
        </Drawer.Content>
      </Drawer>
    </Container>
  )
}
```

This adds a Create button in the Digital Products UI route and, when it’s clicked, shows the form in a drawer or side window.

You pass to the `CreateDigitalProductForm` component an `onSuccess` prop that, when the digital product is created successfully, re-fetches the digital products.

### Test Create Form Out

To test the form, open the Digital Products page in the Medusa Admin. There, you’ll find a new Create button.

If you click on the button, a form will open in a drawer. Fill in the details of the digital product to create one.

After you create the digital product, you’ll find it in the table. You can also click on View Product to edit the product’s details, such as the variant’s price.

To use this digital product in later steps (such as to create an order), you must make the following changes to its associated product details:

1. Change the status to published.
2. Add it to the default sales channel.
3. Add prices to the variant.

***

## Step 11: Handle Product Deletion

When a product is deleted, its product variants are also deleted, meaning that their associated digital products should also be deleted.

In this step, you'll build a flow that deletes the digital products associated with a deleted product's variants. Then, you'll execute this workflow whenever a product is deleted.

The workflow has the following steps:

- `retrieveDigitalProductsToDeleteStep`: Retrieve the digital products associated with a deleted product's variants.
- `deleteDigitalProductsStep`: Delete the digital products.

### retrieveDigitalProductsToDeleteStep

The first step of the workflow receives the ID of the deleted product as an input and retrieves the digital products associated with its variants.

Create the file `src/workflows/delete-product-digital-products/steps/retrieve-digital-products-to-delete.ts` with the following content:

```ts title="src/workflows/delete-product-digital-products/steps/retrieve-digital-products-to-delete.ts" highlights={retrieveDigitalProductsHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import DigitalProductVariantLink from "../../../links/digital-product-variant"

type RetrieveDigitalProductsToDeleteStepInput = {
  product_id: string
}

export const retrieveDigitalProductsToDeleteStep = createStep(
  "retrieve-digital-products-to-delete",
  async ({ product_id }: RetrieveDigitalProductsToDeleteStepInput, { container }) => {
    const productService = container.resolve("product")
    const query = container.resolve("query")

    const productVariants = await productService.listProductVariants({
      product_id: product_id,
    }, {
      withDeleted: true,
    })

    const { data } = await query.graph({
      entity: DigitalProductVariantLink.entryPoint,
      fields: ["digital_product.*"],
      filters: {
        product_variant_id: productVariants.map((v) => v.id),
      },
    })

    const digitalProductIds = data.map((d) => d.digital_product.id)

    return new StepResponse(digitalProductIds)
  }
)
```

You create a `retrieveDigitalProductsToDeleteStep` step that retrieves the product variants of the deleted product. Notice that you pass in the second object parameter of `listProductVariants` a `withDeleted` property that ensures deleted variants are included in the result.

Then, you use Query to retrieve the digital products associated with the product variants. Links created with `defineLink` have an `entryPoint` property that you can use with Query to retrieve data from the pivot table of the link between the data models.

Finally, you return the IDs of the digital products to delete.

### deleteDigitalProductsSteps

Next, you'll implement the step that deletes those digital products.

Create the file `src/workflows/delete-product-digital-products/steps/delete-digital-products.ts` with the following content:

```ts title="src/workflows/delete-product-digital-products/steps/delete-digital-products.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { DIGITAL_PRODUCT_MODULE } from "../../../modules/digital-product"
import DigitalProductModuleService from "../../../modules/digital-product/service"

type DeleteDigitalProductsStep = {
  ids: string[]
}

export const deleteDigitalProductsSteps = createStep(
  "delete-digital-products",
  async ({ ids }: DeleteDigitalProductsStep, { container }) => {
    const digitalProductService: DigitalProductModuleService = 
      container.resolve(DIGITAL_PRODUCT_MODULE)

    await digitalProductService.softDeleteDigitalProducts(ids)

    return new StepResponse({}, ids)
  },
  async (ids, { container }) => {
    if (!ids) {
      return
    }

    const digitalProductService: DigitalProductModuleService = 
      container.resolve(DIGITAL_PRODUCT_MODULE)

    await digitalProductService.restoreDigitalProducts(ids)
  }
)
```

In the `deleteDigitalProductsSteps`, you soft delete the digital products by the ID passed as a parameter. In the compensation function, you restore the digital products if an error occurs.

### Create deleteProductDigitalProductsWorkflow

You can now create the workflow that executes those steps.

Create the file `src/workflows/delete-product-digital-products/index.ts` with the following content:

```ts title="src/workflows/delete-product-digital-products/index.ts"
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { deleteDigitalProductsSteps } from "./steps/delete-digital-products"
import { retrieveDigitalProductsToDeleteStep } from "./steps/retrieve-digital-products-to-delete"

type DeleteProductDigitalProductsInput = {
  id: string
}

export const deleteProductDigitalProductsWorkflow = createWorkflow(
  "delete-product-digital-products",
  (input: DeleteProductDigitalProductsInput) => {
    const digitalProductsToDelete = retrieveDigitalProductsToDeleteStep({
      product_id: input.id,
    })

    deleteDigitalProductsSteps({
      ids: digitalProductsToDelete,
    })

    return new WorkflowResponse({})
  }
)
```

The `deleteProductDigitalProductsWorkflow` receives the ID of the deleted product as an input. In the workflow, you:

- Run the `retrieveDigitalProductsToDeleteStep` to retrieve the digital products associated with the deleted product.
- Run the `deleteDigitalProductsSteps` to delete the digital products.

### Execute Workflow on Product Deletion

When a product is deleted, Medusa emits a `product.deleted` event. You can handle this event with a subscriber. A subscriber is an asynchronous function that, when an event is emitted, is executed. You can implement in subscribers features that aren't essential to the original flow that emitted the event.

Learn more about subscribers in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).

So, you'll listen to the `product.deleted` event in a subscriber, and execute the workflow whenever the product is deleted.

Create the file `src/subscribers/handle-product-deleted.ts` with the following content:

```ts title="src/subscribers/handle-product-deleted.ts"
import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
import { 
  deleteProductDigitalProductsWorkflow,
} from "../workflows/delete-product-digital-products"

export default async function handleProductDeleted({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await deleteProductDigitalProductsWorkflow(container)
    .run({
      input: data,
    })
}

export const config: SubscriberConfig = {
  event: "product.deleted",
}
```

A subscriber file must export:

- An asynchronous function that's executed whenever the specified event is emitted.
- A configuration object that specifies the event the subscriber listens to, which is in this case `product.deleted`.

The subscriber function receives as a parameter an object having the following properties:

- `event`: An object containing the data payload of the emitted event.
- `container`: Instance of the [Medusa Container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

In the subscriber, you execute the workflow by invoking it, passing the Medusa container as an input, then executing its `run` method. You pass the product's ID, which is received through the event's data payload, as an input to the workflow.

### Test it Out

To test this out, start the Medusa application and, from the Medusa Admin dashboard, delete a product that has digital products. You can confirm that the digital product was deleted by checking the Digital Products page.

***

## Step 12: Create Digital Product Fulfillment Module Provider

In this step, you'll create a fulfillment module provider for digital products. It doesn't have any real fulfillment functionality as digital products aren't physically fulfilled.

### Create Module Provider Service

Start by creating the `src/modules/digital-product-fulfillment` directory.

Then, create the file `src/modules/digital-product-fulfillment/service.ts` with the following content:

```ts title="src/modules/digital-product-fulfillment/service.ts"
import { AbstractFulfillmentProviderService } from "@medusajs/framework/utils"
import { 
  CreateFulfillmentResult, 
  FulfillmentDTO, 
  FulfillmentItemDTO, 
  FulfillmentOption, 
  FulfillmentOrderDTO,
} from "@medusajs/framework/types"

class DigitalProductFulfillmentService extends AbstractFulfillmentProviderService {
  static identifier = "digital"

  constructor() {
    super()
  }

  async getFulfillmentOptions(): Promise<FulfillmentOption[]> {
    return [
      {
        id: "digital-fulfillment",
      },
    ]
  }

  async validateFulfillmentData(
    optionData: Record<string, unknown>,
    data: Record<string, unknown>,
    context: Record<string, unknown>
  ): Promise<any> {
    return data
  }

  async validateOption(data: Record<string, any>): Promise<boolean> {
    return true
  }

  async createFulfillment(
    data: Record<string, unknown>, 
    items: Partial<Omit<FulfillmentItemDTO, "fulfillment">>[], 
    order: Partial<FulfillmentOrderDTO> | undefined, 
    fulfillment: Partial<Omit<FulfillmentDTO, "provider_id" | "data" | "items">>
  ): Promise<CreateFulfillmentResult> {
    // No data is being sent anywhere
    return {
      data,
      labels: [],
    }
  }

  async cancelFulfillment(): Promise<any> {
    return {}
  }

  async createReturnFulfillment(): Promise<any> {
    return {}
  }
}

export default DigitalProductFulfillmentService
```

The fulfillment provider registers one fulfillment option, and doesn't perform actual fulfillment.

### Create Module Provider Definition

Then, create the module provider's definition in the file `src/modules/digital-product-fulfillment/index.ts`:

```ts title="src/modules/digital-product-fulfillment/index.ts"
import { ModuleProviderExports } from "@medusajs/framework/types"
import DigitalProductFulfillmentService from "./service"

const services = [DigitalProductFulfillmentService]

const providerExport: ModuleProviderExports = {
  services,
}

export default providerExport
```

### Register Module Provider in Medusa's Configurations

Finally, register the module provider in `medusa-config.ts`:

```ts title="medusa-config.ts"
// other imports...
import { Modules } from "@medusajs/framework/utils"

module.exports = defineConfig({
  modules: [
    // ...
    {
      resolve: "@medusajs/medusa/fulfillment",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/fulfillment-manual",
            id: "manual",
          },
          {
            resolve: "./src/modules/digital-product-fulfillment",
            id: "digital",
          },
        ],
      },
    },
  ],
})
```

This registers the digital product fulfillment as a module provider of the Fulfillment Module.

### Add Fulfillment Provider to Location

In the Medusa Admin, go to Settings -> Location & Shipping, and add the fulfillment provider and a shipping option for it in a location.

This is necessary to use the fulfillment provider's shipping option during checkout.

***

## Step 13: Create Cart Completion Flow for Digital Products

In this step, you’ll create a new cart completion flow that not only creates a Medusa order, but also create a digital product order.

To create the cart completion flow, you’ll create a workflow and then use that workflow in an API route defined at `src/api/store/carts/[id]/complete-digital/route.ts`.

```mermaid
graph TD
  completeCartWorkflow["completeCartWorkflow (Medusa)"] --> useQueryGraphStep["useQueryGraphStep (Medusa)"]
  useQueryGraphStep --> when{order has digital products?}
  when -->|Yes| createDigitalProductOrderStep
  createDigitalProductOrderStep --> createRemoteLinkStep["createRemoteLinkStep (Medusa)"]
  createRemoteLinkStep --> createOrderFulfillmentWorkflow["createOrderFulfillmentWorkflow (Medusa)"]
  createOrderFulfillmentWorkflow --> emitEventStep["emitEventStep (Medusa)"]
  emitEventStep --> End
  when -->|No| End
```

The workflow has the following steps:

1. `completeCartWorkflow` to create a Medusa order from the cart. Medusa provides this workflow through the `@medusajs/medusa/core-flows` package and you can use it as a step.
2. `useQueryGraphStep` to retrieve the order’s items with the digital products associated with the purchased product variants. Medusa provides this step through the `@medusajs/medusa/core-flows` package.
3. If the order has digital products, you:
   1. create the digital product order.
   2. link the digital product order with the Medusa order. Medusa provides a `createRemoteLinkStep` in the `@medusajs/medusa/core-flows` package that can be used here.
   3. Create a fulfillment for the digital products in the order. Medusa provides a `createOrderFulfillmentWorkflow` in the `@medusajs/medusa/core-flows` package that you can use as a step here.
   4. Emit the `digital_product_order.created` custom event to handle it later in a subscriber and send the customer an email. Medusa provides a `emitEventStep` in the `@medusajs/medusa/core-flows` that you can use as a step here.

You’ll only implement the `3.a` step of the workflow.

### createDigitalProductOrderStep (Step 3.a)

Create the file `src/workflows/create-digital-product-order/steps/create-digital-product-order.ts` with the following content:

```ts title="src/workflows/create-digital-product-order/steps/create-digital-product-order.ts" highlights={createDpoHighlights} collapsibleLines="1-14" expandMoreLabel="Show Imports"
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  OrderLineItemDTO,
  ProductVariantDTO,
  InferTypeOf,
} from "@medusajs/framework/types"
import { OrderStatus } from "../../../modules/digital-product/types"
import DigitalProductModuleService from "../../../modules/digital-product/service"
import { DIGITAL_PRODUCT_MODULE } from "../../../modules/digital-product"
import DigitalProduct from "../../../modules/digital-product/models/digital-product"

export type CreateDigitalProductOrderStepInput = {
  items: (OrderLineItemDTO & {
    variant: ProductVariantDTO & {
      digital_product: InferTypeOf<typeof DigitalProduct>
    }
  })[]
}

const createDigitalProductOrderStep = createStep(
  "create-digital-product-order",
  async ({ items }: CreateDigitalProductOrderStepInput, { container }) => {
    const digitalProductModuleService: DigitalProductModuleService = 
      container.resolve(DIGITAL_PRODUCT_MODULE)

    const digitalProductIds = items.map((item) => item.variant.digital_product.id)

    const digitalProductOrder = await digitalProductModuleService
      .createDigitalProductOrders({
        status: OrderStatus.PENDING,
        products: digitalProductIds,
      })

    return new StepResponse({
      digital_product_order: digitalProductOrder,
    }, {
      digital_product_order: digitalProductOrder,
    })
  },
  async (data, { container }) => {
    if (!data) {
      return
    }
    const digitalProductModuleService: DigitalProductModuleService = 
      container.resolve(DIGITAL_PRODUCT_MODULE)

    await digitalProductModuleService.deleteDigitalProductOrders(
      data.digital_product_order.id
    )
  }
)

export default createDigitalProductOrderStep
```

This creates the `createDigitalProductOrderStep`. In this step, you create a digital product order.

In the compensation function, you delete the digital product order.

### Create createDigitalProductOrderWorkflow

Create the file `src/workflows/create-digital-product-order/index.ts` with the following content:

```ts title="src/workflows/create-digital-product-order/index.ts" highlights={createDpoWorkflowHighlights} collapsibleLines="1-17" expandMoreLabel="Show Imports"
import {
  createWorkflow,
  transform,
  when,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
  completeCartWorkflow,
  useQueryGraphStep,
  createRemoteLinkStep,
  createOrderFulfillmentWorkflow,
  emitEventStep,
  acquireLockStep,
  releaseLockStep,
} from "@medusajs/medusa/core-flows"
import {
  Modules,
} from "@medusajs/framework/utils"
import createDigitalProductOrderStep, { 
  CreateDigitalProductOrderStepInput,
} from "./steps/create-digital-product-order"
import { DIGITAL_PRODUCT_MODULE } from "../../modules/digital-product"
import digitalProductOrderOrderLink from "../../links/digital-product-order"

type WorkflowInput = {
  cart_id: string
}

const createDigitalProductOrderWorkflow = createWorkflow(
  "create-digital-product-order",
  (input: WorkflowInput) => {
    acquireLockStep({
      key: input.cart_id,
      timeout: 30,
      ttl: 120,
    })
    const { id } = completeCartWorkflow.runAsStep({
      input: {
        id: input.cart_id,
      },
    })

    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "*",
        "items.*",
        "items.variant.*",
        "items.variant.digital_product.*",
        "shipping_address.*",
      ],
      filters: {
        id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const { data: existingLinks } = useQueryGraphStep({
      entity: digitalProductOrderOrderLink.entryPoint,
      fields: ["digital_product_order.id"],
      filters: { order_id: id },
    }).config({ name: "retrieve-existing-links" })

    const itemsWithDigitalProducts = transform(
      {
        orders,
      },
      (data) => {
        return data.orders[0].items?.filter(
          (item) => item?.variant?.digital_product !== undefined
        )
      }
    )

    const digital_product_order = when(
      "create-digital-product-order-condition",
      { itemsWithDigitalProducts, existingLinks },
      (data) => {
        return (
          !!data.itemsWithDigitalProducts?.length &&
          data.existingLinks.length === 0
        )
      }
    ).then(() => {
      const { 
        digital_product_order,
      } = createDigitalProductOrderStep({
        items: orders[0].items,
      } as unknown as CreateDigitalProductOrderStepInput)
  
      createRemoteLinkStep([{
        [DIGITAL_PRODUCT_MODULE]: {
          digital_product_order_id: digital_product_order.id,
        },
        [Modules.ORDER]: {
          order_id: id,
        },
      }])

      createOrderFulfillmentWorkflow.runAsStep({
        input: {
          order_id: id,
          items: transform({
            itemsWithDigitalProducts,
          }, (data) => {
            return data.itemsWithDigitalProducts!.map((item) => ({
              id: item!.id,
              quantity: item!.quantity,
            }))
          }),
        },
      })
  
      emitEventStep({
        eventName: "digital_product_order.created",
        data: {
          id: digital_product_order.id,
        },
      })

      return digital_product_order
    })

    releaseLockStep({
      key: input.cart_id,
    })

    return new WorkflowResponse({
      order: orders[0],
      digital_product_order,
    })
  }
)

export default createDigitalProductOrderWorkflow
```

This creates the workflow `createDigitalProductOrderWorkflow`. It runs the following steps:

1. `acquireLockStep` to acquire a lock on the cart ID.
2. `completeCartWorkflow` as a step to create the Medusa order.
3. `useQueryGraphStep` to retrieve the order’s items with their associated variants and linked digital products.
4. `useQueryGraphStep` to retrieve any existing links between the digital product order and the Medusa order.
   - This is necessary to ensure the workflow is idempotent, meaning that if the workflow is executed multiple times for the same cart, it doesn’t create multiple digital product orders.
5. Use `transform` to filter the order’s items to only those having digital products.
6. Use `when` to check whether the order has digital products and no existing links. If so:
   1. Use the `createDigitalProductOrderStep` to create the digital product order.
   2. Use the `createRemoteLinkStep` to link the digital product order to the Medusa order.
   3. Use the `createOrderFulfillmentWorkflow` to create a fulfillment for the digital products in the order.
   4. Use the `emitEventStep` to emit a custom event.
7. `releaseLockStep` to release the lock on the cart ID.

The workflow returns the Medusa order and the digital product order, if created.

### Cart Completion API Route

Next, create the file `src/api/store/carts/[id]/complete-digital/route.ts` with the following content:

```ts title="src/api/store/carts/[id]/complete-digital/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import createDigitalProductOrderWorkflow from "../../../../../workflows/create-digital-product-order"

export const POST = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const { result } = await createDigitalProductOrderWorkflow(req.scope)
    .run({
      input: {
        cart_id: req.params.id,
      },
    })

  res.json({
    type: "order",
    ...result,
  })
}
```

Since you export a `POST` function, you expose a `POST` API route at `/store/carts/[id]/complete-digital`.

In the route handler, you execute the `createDigitalProductOrderWorkflow` and return the created order in the response.

### Test Cart Completion: Customize Next.js Starter Storefront

To test out the cart completion, you'll customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) that you installed in the first step to use the new cart completion route to place an order.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-digital-product`, you can find the storefront by going back to the parent directory and changing to the `medusa-digital-product-storefront` directory:

```bash
cd ../medusa-digital-product-storefront # change based on your project name
```

In the Next.js Starter Storefront, open the file `src/lib/data/cart.ts` and find the following lines in the `placeOrder` function:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
const cartRes = await sdk.store.cart
  .complete(id, {}, headers)
```

Replace these lines with the following:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
const cartRes = await sdk.client.fetch<HttpTypes.StoreCompleteCartResponse>(
  `/store/carts/${id}/complete-digital`,
    {
      method: "POST",
      headers,
    }
  )
```

This will send a `POST` request to the new cart completion route you created to complete the cart.

Then, run the following command in the Medusa application directory to start the Medusa application:

```bash npm2yarn badgeLabel="Medusa application" badgeColor="green"
npm run start
```

And run the following command in the Next.js Starter Storefront directory to start the Next.js application:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

Open the storefront in your browser at `http://localhost:8000` and add a digital product to the cart.

Then, go through the checkout process. Make sure to choose the shipping option you created in the previous step for shipping.

Once you place the order, the cart completion route you added above will run, creating the order and digital product order, if the order has digital products.

In a later step, you’ll add an API route to allow customers to view and download their purchased digital products.

### Further Read

- [Conditions in Workflows with When-Then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md)

***

## Step 14: Fulfill Digital Order Workflow

In this step, you'll create a workflow that fulfills a digital order by sending a notification to the customer. Later, you'll execute this workflow in a subscriber that listens to the `digital_product_order.created` event.

The workflow has the following steps:

1. Retrieve the digital product order's details. For this, you'll use `useQueryGraphStep` from Medusa's core workflows.
2. Send a notification to the customer with the digital products to download.
3. Mark the Medusa order's fulfillment as delivered. For this, you'll use `markOrderFulfillmentAsDeliveredWorkflow` from Medusa's core workflows.

So, you only need to implement the second step.

### Add Types

Before creating the step, add to `src/modules/digital-product/types/index.ts` the following:

```ts
import { OrderDTO, InferTypeOf } from "@medusajs/framework/types"
import DigitalProductOrder from "../models/digital-product-order"

// ...

export type DigitalProductOrder = 
  InferTypeOf<typeof DigitalProductOrder> & {
    order?: OrderDTO
  }
```

This adds a type for a digital product order, which you'll use next.

You use `InferTypeOf` to infer the type of the `DigitalProductOrder` data model, and add to it the optional `order` property, which is the linked order.

### Create sendDigitalOrderNotificationStep

To create the step, create the file `src/workflows/fulfill-digital-order/steps/send-digital-order-notification.ts` with the following content:

```ts title="src/workflows/fulfill-digital-order/steps/send-digital-order-notification.ts" collapsibleLines="1-11" expandMoreLabel="Show Imports"
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  INotificationModuleService,
  IFileModuleService,
} from "@medusajs/framework/types"
import { 
  MedusaError, 
  ModuleRegistrationName, 
  promiseAll,
} from "@medusajs/framework/utils"
import { DigitalProductOrder, MediaType } from "../../../modules/digital-product/types"

export type SendDigitalOrderNotificationStepInput = {
  digital_product_order: DigitalProductOrder
}

export const sendDigitalOrderNotificationStep = createStep(
  "send-digital-order-notification",
  async ({ 
    digital_product_order: digitalProductOrder, 
  }: SendDigitalOrderNotificationStepInput, 
  { container }) => {
    const notificationModuleService: INotificationModuleService = container
    .resolve(ModuleRegistrationName.NOTIFICATION)
    const fileModuleService: IFileModuleService = container.resolve(
      ModuleRegistrationName.FILE
    )

    if (!digitalProductOrder.order) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Digital product order is missing associated order."
      )
    }

    if (!digitalProductOrder.order.email) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Order is missing email."
      )
    }

    // TODO assemble notification
  }
)
```

This creates the `sendDigitalOrderNotificationStep` step that receives a digital product order as an input.

In the step, so far you resolve the main services of the Notification and File Modules. You also check that the digital product order has an associated order and that the order has an email.

Next, you'll prepare the data to pass in the notification. Replace the `TODO` with the following:

```ts title="src/workflows/fulfill-digital-order/steps/send-digital-order-notification.ts"
const notificationData = await promiseAll(
  digitalProductOrder.products.map(async (product) => {
    const medias: string[] = []

    await promiseAll(
      product.medias
      .filter((media) => media.type === MediaType.MAIN)
      .map(async (media) => {
        medias.push(
          (await fileModuleService.retrieveFile(media.fileId)).url
        )
      })
    )

    return {
      name: product.name,
      medias,
    }
  })
)

// TODO send notification
```

In this snippet, you put together the data to send in the notification. You loop over the digital products in the order and retrieve the URL of their main files using the File Module.

Finally, replace the new `TODO` with the following:

```ts title="src/workflows/fulfill-digital-order/steps/send-digital-order-notification.ts"
const notification = await notificationModuleService.createNotifications({
  to: digitalProductOrder.order.email,
  template: "digital-order-template",
  channel: "email",
  data: {
    products: notificationData,
  },
})

return new StepResponse(notification)
```

You use the `createNotifications` method of the Notification Module's main service to send an email using the installed provider.

### Create Workflow

Create the workflow in the file `src/workflows/fulfill-digital-order/index.ts`:

```ts title="src/workflows/fulfill-digital-order/index.ts" highlights={fulfillWorkflowHighlights} collapsibleLines="1-13" expandMoreLabel="Show Imports"
import {
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
  markOrderFulfillmentAsDeliveredWorkflow,
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { 
  sendDigitalOrderNotificationStep, 
  SendDigitalOrderNotificationStepInput,
} from "./steps/send-digital-order-notification"

type FulfillDigitalOrderWorkflowInput = {
  id: string
}

export const fulfillDigitalOrderWorkflow = createWorkflow(
  "fulfill-digital-order",
  ({ id }: FulfillDigitalOrderWorkflowInput) => {
    const { data: digitalProductOrders } = useQueryGraphStep({
      entity: "digital_product_order",
      fields: [
        "*",
        "products.*",
        "products.medias.*",
        "order.*",
        "order.fulfillments.*",
      ],
      filters: {
        id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    sendDigitalOrderNotificationStep({
      digital_product_order: digitalProductOrders[0],
    } as unknown as SendDigitalOrderNotificationStepInput)

    when({ digitalProductOrders }, (data) => !!data.digitalProductOrders[0].order?.fulfillments?.length)
      .then(() => {
        markOrderFulfillmentAsDeliveredWorkflow.runAsStep({
          input: {
            orderId: digitalProductOrders[0].order!.id,
            fulfillmentId: digitalProductOrders[0].order!.fulfillments![0]!.id,
          },
        })
      })

    return new WorkflowResponse(
      digitalProductOrders[0]
    )
  }
)
```

In the workflow, you:

1. Retrieve the digital product order's details using `useQueryGraphStep` from Medusa's core workflows.
2. Send a notification to the customer with the digital product download links using the `sendDigitalOrderNotificationStep`.
3. Check whether the Medusa order has fulfillments. If so, mark the order's fulfillment as delivered using `markOrderFulfillmentAsDeliveredWorkflow` from Medusa's core workflows.

`when` allows you to perform steps based on a condition during execution. Learn more in the [Conditions in Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) documentation.

### Configure Notification Module Provider

In the `sendDigitalOrderNotificationStep`, you use a notification provider configured for the `email` channel to send the notification.

Check out the [Integrations page](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/integrations/index.html.md) to find Notification Module Providers.

For testing purposes, add to `medusa-config.ts` the following to use the Local Notification Module Provider:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    // ...
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/notification-local",
            id: "local",
            options: {
              name: "Local Notification Provider",
              channels: ["email"],
            },
          },
        ],
      },
    },
  ],
})

```

***

## Step 15: Handle the Digital Product Order Event

In this step, you'll create a subscriber that listens to the `digital_product_order.created` event and executes the workflow from the above step.

Create the file `src/subscribers/handle-digital-order.ts` with the following content:

```ts title="src/subscribers/handle-digital-order.ts" collapsibleLines="1-8" expandMoreLabel="Show Imports"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { 
  fulfillDigitalOrderWorkflow,
} from "../workflows/fulfill-digital-order"

async function digitalProductOrderCreatedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await fulfillDigitalOrderWorkflow(container).run({
    input: {
      id: data.id,
    },
  })
}

export default digitalProductOrderCreatedHandler

export const config: SubscriberConfig = {
  event: "digital_product_order.created",
}
```

This adds a subscriber that listens to the `digital_product_order.created` event. It executes the `fulfillDigitalOrderWorkflow` to send the customer an email and mark the order's fulfillment as fulfilled.

### Test Subscriber Out

To test out the subscriber, place an order with digital products. This triggers the `digital_product_order.created` event which executes the subscriber.

***

## Step 16: Create Store API Routes

In this step, you’ll create three store API routes:

1. Retrieve the preview files of a digital product. This is useful when the customer is browsing the products before purchase.
2. List the digital products that the customer has purchased.
3. Get the download link to a media of the digital product that the customer purchased.

### Retrieve Digital Product Previews API Route

Create the file `src/api/store/digital-products/[id]/preview/route.ts` with the following content:

```ts title="src/api/store/digital-products/[id]/preview/route.ts" highlights={previewRouteHighlights} collapsibleLines="1-15" expandMoreLabel="Show Imports"
import { 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import {
  Modules,
} from "@medusajs/framework/utils"
import { 
  DIGITAL_PRODUCT_MODULE,
} from "../../../../../modules/digital-product"
import DigitalProductModuleService from "../../../../../modules/digital-product/service"
import { 
  MediaType,
} from "../../../../../modules/digital-product/types"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const fileModuleService = req.scope.resolve(
    Modules.FILE
  )

  const digitalProductModuleService: DigitalProductModuleService = 
    req.scope.resolve(
      DIGITAL_PRODUCT_MODULE
    )
  
  const medias = await digitalProductModuleService.listDigitalProductMedias({
    digital_product_id: req.params.id,
    type: MediaType.PREVIEW,
  })

  const normalizedMedias = await Promise.all(
    medias.map(async (media) => {
      const { fileId, ...mediaData } = media
      const fileData = await fileModuleService.retrieveFile(fileId)

      return {
        ...mediaData,
        url: fileData.url,
      }
    })
  )

  res.json({
    previews: normalizedMedias,
  })
}
```

This adds a `GET` API route at `/store/digital-products/[id]/preview`, where `[id]` is the ID of the digital product to retrieve its preview media.

In the route handler, you retrieve the preview media of the digital product and then use the File Module’s service to get the URL of the preview file.

You return in the response the preview files.

### List Digital Product Purchases API Route

Create the file `src/api/store/customers/me/digital-products/route.ts` with the following content:

```ts title="src/api/store/customers/me/digital-products/route.ts" highlights={purchasedDpHighlights} collapsibleLines="1-8" expandMoreLabel="Show Imports"
import { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { 
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: [customer] } = await query.graph({
    entity: "customer",
    fields: [
      "orders.digital_product_order.products.*",
      "orders.digital_product_order.products.medias.*",
    ],
    filters: {
      id: req.auth_context.actor_id,
    },
  })

  const digitalProducts = {}

  customer.orders?.forEach((order) => {
    order?.digital_product_order?.products.forEach((product) => {
      if (!product) {
        return
      }
      digitalProducts[product.id] = product
    })
  })

  res.json({
    digital_products: Object.values(digitalProducts),
  })
}
```

This adds a `GET` API route at `/store/customers/me/digital-products`. All API routes under `/store/customers/me` require customer authentication.

In the route handler, you use Query to retrieve the customer’s orders and linked digital product orders, and return the purchased digital products in the response.

### Get Digital Product Media Download URL API Route

Create the file `src/api/store/customers/me/digital-products/[mediaId]/download/route.ts` with the following content:

```ts title="src/api/store/customers/me/digital-products/[mediaId]/download/route.ts" highlights={downloadUrlHighlights} collapsibleLines="1-10" expandMoreLabel="Show Imports"
import { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { 
  Modules,
  ContainerRegistrationKeys,
  MedusaError,
} from "@medusajs/framework/utils"

export const POST = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const fileModuleService = req.scope.resolve(
    Modules.FILE
  )
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: [customer] } = await query.graph({
    entity: "customer",
    fields: [
      "orders.digital_product_order.*",
    ],
    filters: {
      id: req.auth_context.actor_id,
    },
  })

  const customerDigitalOrderIds = customer.orders?.filter(
    (order) => order?.digital_product_order !== undefined
  ).map((order) => order!.digital_product_order!.id)

  const { data: dpoResult } = await query.graph({
    entity: "digital_product_order",
    fields: [
      "products.medias.*",
    ],
    filters: {
      id: customerDigitalOrderIds,
    },
  })

  if (!dpoResult.length) {
    throw new MedusaError(
      MedusaError.Types.NOT_ALLOWED,
      "Customer didn't purchase digital product."
    )
  }

  let foundMedia: any | undefined = undefined

  dpoResult[0].products.some((product) => {
    return product?.medias.some((media) => {
      foundMedia = media?.id === req.params.mediaId ? media : undefined

      return foundMedia !== undefined
    })
  })

  if (!foundMedia) {
    throw new MedusaError(
      MedusaError.Types.NOT_ALLOWED,
      "Customer didn't purchase digital product."
    )
  }

  const fileData = await fileModuleService.retrieveFile(foundMedia.fileId)

  res.json({
    url: fileData.url,
  })
}
```

This adds a `POST` API route at `/store/customers/me/digital-products/[mediaId]`, where `[mediaId]` is the ID of the digital product media to download.

In the route handler, you retrieve the customer’s orders and linked digital orders, then check if the digital orders have the required media file. If not, an error is thrown.

If the media is found in th customer's previous purchases, you use the File Module’s service to retrieve the download URL of the media and return it in the response.

You’ll test out these API routes in the next step.

### Further Reads

- [What are protected API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md)

***

## Step 17: Customize Next.js Starter

In this section, you’ll customize the [Next.js Starter storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to:

1. Show a preview button on a digital product’s page to view the preview files.
2. Add a new tab in the customer’s dashboard to view their purchased digital products.
3. Allow customers to download the digital products through the new page in the dashboard.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-digital-product`, you can find the storefront by going back to the parent directory and changing to the `medusa-digital-product-storefront` directory:

```bash
cd ../medusa-digital-product-storefront # change based on your project name
```

### Add Types

In `src/types/global.ts`, add the following types that you’ll use in your customizations:

```ts title="src/types/global.ts" badgeLabel="Storefront" badgeColor="blue"
import { 
  // other imports...
  StoreProductVariant,
} from "@medusajs/types"

// ...

export type DigitalProduct = {
  id: string
  name: string
  medias?: DigitalProductMedia[]
}

export type DigitalProductMedia = {
  id: string
  fileId: string
  type: "preview" | "main"
  mimeType: string
  digitalProduct?: DigitalProduct[]
}

export type DigitalProductPreview = DigitalProductMedia & {
  url: string
}

export type VariantWithDigitalProduct = StoreProductVariant & {
  digital_product?: DigitalProduct
}

```

### Retrieve Digital Products with Variants

To retrieve the digital products details when retrieving a product and its variants, in the `src/lib/data/products.ts` file, change the `listProducts` function to pass the digital products in the `fields` property passed to the `sdk.store.product.list` method:

```ts title="src/lib/data/products.ts" highlights={fieldHighlights} badgeLabel="Storefront" badgeColor="blue"
export const listProducts = async ({
  pageParam = 1,
  queryParams,
  countryCode,
  regionId,
}: {
  pageParam?: number
  queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams
  countryCode?: string
  regionId?: string
}): Promise<{
  response: { products: HttpTypes.StoreProduct[]; count: number }
  nextPage: number | null
  queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams
}> => {
  // ...
  return sdk.client
    .fetch<{ products: HttpTypes.StoreProduct[]; count: number }>(
      `/store/products`,
      {
        // ...
        query: {
          // ...
          fields: "*variants.calculated_price,+variants.inventory_quantity,+metadata,+tags,*variants.calculated_price,*variants.digital_product",
        },
      }
    )
    // ...
}
```

When a customer views a product’s details page, digital products linked to variants are also retrieved.

### Get Digital Product Preview Links

To retrieve the links of a digital product’s preview media, first, add the following import at the top of `src/lib/data/products.ts`:

```ts title="src/lib/data/products.ts" badgeLabel="Storefront" badgeColor="blue"
import { DigitalProductPreview } from "../../types/global"
```

Then, add the following function at the end of the file:

```ts title="src/lib/data/products.ts"
export const getDigitalProductPreview = async function ({
  id,
}: {
  id: string
}) {
  const headers = {
    ...(await getAuthHeaders()),
  }

  const next = {
    ...(await getCacheOptions("products")),
  }
  const { previews } = await sdk.client.fetch<{
    previews: DigitalProductPreview[]
  }>(
    `/store/digital-products/${id}/preview`, 
    {
      headers,
      next,
      cache: "force-cache",
    }
  )

  // for simplicity, return only the first preview url
  // instead you can show all the preview media to the customer
  return previews.length ? previews[0].url : ""
}
```

This function uses the API route you created in the previous section to get the preview links and return the first preview link.

### Add Preview Button

To add a button that shows the customer the preview media of a digital product, first, in `src/modules/products/components/product-actions/index.tsx`, cast the `selectedVariant` variable in the component to the `VariantWithDigitalProduct` type you created earlier:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
// other imports...
import { VariantWithDigitalProduct } from "../../../../types/global"

export default function ProductActions({
  product,
  region,
  disabled,
}: ProductActionsProps) {

  // ...
    
  const selectedVariant = useMemo(() => {
    // ...
  }, [product.variants, options]) as VariantWithDigitalProduct
    
  // ...
}
```

Then, add the following function in the component:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
// other imports...
import { getDigitalProductPreview } from "../../../../lib/data/products"

export default function ProductActions({
  product,
  region,
  disabled,
}: ProductActionsProps) {
  // ...
    
  const handleDownloadPreview = async () => {
    if (!selectedVariant?.digital_product) {
      return
    }

    const downloadUrl = await getDigitalProductPreview({
      id: selectedVariant?.digital_product.id,
    })

    if (downloadUrl.length) {
      window.open(downloadUrl)
    }
  }
    
  // ...
}
```

This function uses the `getDigitalProductPreview` function you created earlier to retrieve the preview URL of the selected variant’s digital product.

Finally, in the `return` statement, add a new button above the add-to-cart button:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div>
    {/* Before add to cart */}
    {selectedVariant?.digital_product && (
      <Button
        onClick={handleDownloadPreview}
        variant="secondary"
        className="w-full h-10"
      >
        Download Preview
      </Button>
      )}
  </div>
)
```

This button is only shown if the selected variant has a digital product. When it’s clicked, the preview URL is retrieved to show the preview media to the customer.

### Test Preview Out

To test it out, run the Next.js starter with the Medusa application, then open the details page of a product that’s digital. You should see a “Download Preview” button to download the preview media of the product.

### Add Digital Purchases Page

You’ll now create the page customers can view their purchased digital product in.

Start by creating the file `src/lib/data/digital-products.ts` with the following content:

```ts title="src/lib/data/digital-products.ts" badgeLabel="Storefront" badgeColor="blue"
"use server"

import { DigitalProduct } from "../../types/global"
import { sdk } from "../config"
import { getAuthHeaders, getCacheOptions } from "./cookies"

export const getCustomerDigitalProducts = async () => {
  const headers = {
    ...(await getAuthHeaders()),
  }

  const next = {
    ...(await getCacheOptions("products")),
  }
  const { digital_products } = await sdk.client.fetch<{
    digital_products: DigitalProduct[]
  }>(`/store/customers/me/digital-products`, {
    
    headers,
    next,
   cache: "no-cache",
  })

  return digital_products as DigitalProduct[]
}
```

The `getCustomerDigitalProducts` retrieves the logged-in customer’s purchased digital products by sending a request to the API route you created earlier.

Then, create the file `src/modules/account/components/digital-products-list/index.tsx` with the following content:

```tsx title="src/modules/account/components/digital-products-list/index.tsx" badgeLabel="Storefront" badgeColor="blue"
"use client"

import { Table } from "@medusajs/ui"
import { DigitalProduct } from "../../../../types/global"

type Props = {
  digitalProducts: DigitalProduct[]
}

export const DigitalProductsList = ({
  digitalProducts,
}: Props) => {
  return (
    <Table>
      <Table.Header>
        <Table.Row>
          <Table.HeaderCell>Name</Table.HeaderCell>
          <Table.HeaderCell>Action</Table.HeaderCell>
        </Table.Row>
      </Table.Header>
      <Table.Body>
        {digitalProducts.map((digitalProduct) => {
          const medias = digitalProduct.medias?.filter((media) => media.type === "main")
          const showMediaCount = (medias?.length || 0) > 1
          return (
            <Table.Row key={digitalProduct.id}>
              <Table.Cell>
                {digitalProduct.name}
              </Table.Cell>
              <Table.Cell>
                <ul>
                  {medias?.map((media, index) => (
                    <li key={media.id}>
                      <a href="#">
                        Download{showMediaCount ? ` ${index + 1}` : ``}
                      </a>
                    </li>
                  ))}
                </ul>
              </Table.Cell>
            </Table.Row>
          )
        })}
      </Table.Body>
    </Table>
  )
}
```

This adds a `DigitalProductsList` component that receives a list of digital products and shows them in a table. Each digital product’s media has a download link. You’ll implement its functionality afterwards.

Next, create the file `src/app/[countryCode]/(main)/account/@dashboard/digital-products/page.tsx` with the following content:

```tsx title="src/app/[countryCode]/(main)/account/@dashboard/digital-products/page.tsx" badgeLabel="Storefront" badgeColor="blue"
import { Metadata } from "next"

import { getCustomerDigitalProducts } from "../../../../../../lib/data/digital-products"
import { DigitalProductsList } from "../../../../../../modules/account/components/digital-products-list"

export const metadata: Metadata = {
  title: "Digital Products",
  description: "Overview of your purchased digital products.",
}

export default async function DigitalProducts() {
  const digitalProducts = await getCustomerDigitalProducts()

  return (
    <div className="w-full" data-testid="orders-page-wrapper">
      <div className="mb-8 flex flex-col gap-y-4">
        <h1 className="text-2xl-semi">Digital Products</h1>
        <p className="text-base-regular">
          View the digital products you've purchased and download them.
        </p>
      </div>
      <div>
        <DigitalProductsList digitalProducts={digitalProducts} />
      </div>
    </div>
  )
}
```

This adds a new route in your Next.js application to show the customer’s purchased digital products.

In the route, you retrieve the digital’s products using the `getCustomerDigitalProducts` function and pass them as the prop of the `DigitalProductsList` component.

Finally, to add a tab in the customer’s account dashboard that links to this page, add it in the `src/modules/account/components/account-nav/index.tsx` file:

```tsx title="src/modules/account/components/account-nav/index.tsx" badgeLabel="Storefront" badgeColor="blue"
// other imports...
import { Photo } from "@medusajs/icons"

const AccountNav = ({
  customer,
}: {
  customer: HttpTypes.StoreCustomer | null
}) => {
  // ...
    
  return (
    <div>
      <div className="small:hidden">
        {/* ... */}
        {/* Add before log out */}
        <li>
          <LocalizedClientLink
            href="/account/digital-products"
            className="flex items-center justify-between py-4 border-b border-gray-200 px-8"
            data-testid="digital-products-link"
          >
            <div className="flex items-center gap-x-2">
              <Photo />
              <span>Digital Products</span>
            </div>
            <ChevronDown className="transform -rotate-90" />
          </LocalizedClientLink>
        </li>
        {/* ... */}
      </div>
      <div className="hidden small:block">
        {/* ... */}
        {/* Add before log out */}
        <li>
          <AccountNavLink
            href="/account/digital-products"
            route={route!}
            data-testid="digital-products-link"
          >
            Digital Products
          </AccountNavLink>
        </li>
        {/* ... */}
      </div>
    </div>
  )
}
```

You add a link to the new route before the log out tab both for small and large devices.

### Test Purchased Digital Products Page

To test out this page, first, log-in as a customer and place an order with a digital product.

Then, go to the customer’s account page and click on the new Digital Products tab. You’ll see a table of digital products to download.

### Add Download Link

To add a download link for the purchased digital products’ medias, first, add a new function to `src/lib/data/digital-products.ts`:

```ts title="src/lib/data/digital-products.ts" badgeLabel="Storefront" badgeColor="blue"
export const getDigitalMediaDownloadLink = async (mediaId: string) => {
  const headers = {
    ...(await getAuthHeaders()),
  }

  const next = {
    ...(await getCacheOptions("products")),
  }
  const { url } = await sdk.client.fetch<{
    url: string
  }>(`/store/customers/me/digital-products/${mediaId}/download`, {
    method: "POST",
    headers,
    next,
   cache: "no-cache",
  })

  return url
}
```

In this function, you send a request to the download API route you created earlier to retrieve the download URL of a purchased digital product media.

Then, in `src/modules/account/components/digital-products-list/index.tsx`, import the `getDigitalMediaDownloadLink` at the top of the file:

```tsx title="src/modules/account/components/digital-products-list/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { getDigitalMediaDownloadLink } from "../../../../lib/data/digital-products"
```

And add a `handleDownload` function in the `DigitalProductsList` component:

```tsx title="src/modules/account/components/digital-products-list/index.tsx"
const handleDownload = async (
  e: React.MouseEvent<HTMLAnchorElement, MouseEvent>,
  mediaId: string
) => {
  e.preventDefault()

  const url = await getDigitalMediaDownloadLink(mediaId)

  window.open(url)
}
```

This function uses the `getDigitalMediaDownloadLink` function to get the download link and opens it in a new window.

Finally, add an `onClick` handler to the digital product medias’ link in the return statement:

```tsx title="src/modules/account/components/digital-products-list/index.tsx"
<a href="#" onClick={(e) => handleDownload(e, media.id)}>
  Download{showMediaCount ? ` ${index + 1}` : ``}
</a>
```

### Test Download Purchased Digital Product Media

To test the latest changes out, open the purchased digital products page and click on the Download link of any media in the table. The media’s download link will open in a new page.

***

## Next Steps

The next steps of this example depend on your use case. This section provides some insight into implementing them.

### Storefront Development

Aside from customizing the Next.js Starter storefront, you can also create a custom storefront. Check out the [Storefront Development](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/index.html.md) section to learn how to create a storefront.

### Admin Development

In this recipe, you learned how to customize the admin with UI routes. You can also do further customization using widgets. Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/index.html.md).


# Digital Products Recipe

This recipe provides the general steps to implement digital products in your Medusa application.

Follow the step-by-step [Digital Products Example](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/digital-products/examples/standard/index.html.md) to learn how to implement digital products in your Medusa application.

## Overview

Digital products are products that are stored and delivered electronically. Examples include e-books, software, and digital art.

When the customer buys a digital product, an email is sent to them where they can download the product.

To implement digital products in Medusa, you create a Digital Product Module that introduces the concept of a digital product and link it to existing product concepts in the Product Module.

***

## Install a File Module Provider

A file module provider handles storage functionalities in Medusa. This includes uploading, retrieving, and downloading files, among other features.

You can use a file module provider to store and manage your digital products.

During development, you can use the Local File Module Provider, which is installed by default in your store. For production, you can use module providers like [S3](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/s3/index.html.md) or create your own.

- [File Module Providers](https://docs.medusajs.com/infrastructure-modules/file/index.html.md): Check out available file module providers.
- [Create a File Module Provider](https://docs.medusajs.com/references/file-provider-module/index.html.md): Learn how to create a file module provider.

***

## Create Digital Product Module

Your custom features and functionalities are implemented inside modules. The module is integrated into the Medusa application without any implications on existing functionalities.

You can create a custom module for digital products that holds your custom data models and the service implementing digital-product-related features.

[How to Create a Module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md): Learn how to create a module.

### Create Custom Data Model

A data model represents a table in the database. You can define in your module data models to store data related to your custom features, such as a digital product.

Then, you can link your custom data model to data models from other modules. For example, you can link the digital product model to the Product Module's `ProductVariant` data model.

- [How to Create a Data Model](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md): Learn how to create a data model.
- [Define Module Links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md): Define links between data models.

### Implement Data Management Features

Your module’s main service holds data-management and other related features. Then, in other resources, such as an API route, you can resolve the service from the Medusa container and use its functionalities.

Medusa facilitates implementing data-management features using the service factory. Your module's main service can extend this service factory, and it generates data-management methods for your data models.

[Service Factory](https://docs.medusajs.com/docs/learn/fundamentals/modules/service-factory/index.html.md): Learn about the service factory and how to use it.

***

## Build Flows for Digital Products

Your use case most likely has flows, such as creating digital products, that require multiple steps.

Create workflows to implement these flows, then utilize these workflows in other resources, such as an API route.

In the workflow's steps, you can resolve the Digital Product Module's service and use its data-management methods to manage digital products.

[Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md): Learn how to create a workflow.

***

## Add Custom API Routes

API routes expose your features to external applications, such as the admin dashboard or the storefront.

You can create custom admin API routes that allow merchants to list and create digital products, and store API routes that allow customers to purchase and download digital products.

[API Routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md): Learn how to create an API route.

***

## Customize Admin Dashboard

Based on your use case, you may need to customize the Medusa Admin to add new widgets or pages.

For example, you can create a page that lists all digital products or a widget that allows merchants to view the digital data associated with a product.

The Medusa Admin is an extensible application within your Medusa application. You can customize it by:

- **Widgets**: Adding widgets to existing pages, such as the product page.
- **UI Routes**: Adding new pages to the Medusa Admin, such as a page to manage digital products.
- **Settings Pages**: Adding new pages to the Medusa Admin settings, such as a page to manage digital product settings.

- [Create Admin Widget](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md): Add widgets into existing admin pages.
- [Create Admin UI Routes](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md): Add new pages to your Medusa Admin.

[Create Admin Setting Page](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes#create-settings-page/index.html.md): Add new page to the Medusa Admin settings.

***

## Deliver Digital Products to the Customer

When a customer purchases a digital product, they should receive a link to download it.

The Fulfillment Module handles all logic related to fulfilling orders. It also supports using fulfillment module providers that implement the logic of fulfilling orders with third-party services.

You can create a custom fulfillment module provider that implements the logic of delivering digital products to customers based on your use case.

- [Fulfillment Module](https://docs.medusajs.com/commerce-modules/fulfillment/index.html.md): Learn about the Fulfillment Module.
- [Create Fulfillment Module Provider](https://docs.medusajs.com/references/fulfillment/provider/index.html.md): Learn how to create a fulfillment module provider.

***

## Customize or Build Storefront

Customers use your storefront to browse your digital products and purchase them. You can also provide other helpful features, such as previewing the digital product before purchase.

Medusa provides a Next.js Starter Storefront with standard commerce features including listing products, placing orders, and managing accounts. You can customize the storefront and cater its functionalities to support digital products.

Alternatively, you can build your own storefront using the Medusa APIs. This headless approach gives you the flexibility to build a custom storefront without limitations on which tech stack you use, or the design of the storefront.

- [Next.js Starter Storefront](https://docs.medusajs.com/nextjs-starter/index.html.md): Learn how to install and use the Next.js Starter Storefront.
- [Storefront Guides](https://docs.medusajs.com/storefront-development/index.html.md): Find guides to build your own storefront.


# Ecommerce Recipe

This recipe provides the general steps to create an ecommerce store with Medusa.

## Overview

Businesses use ecommerce stores to:

- Provide an online catalog for customers.
- Accept customer orders and payment.
- Manage their store's data and logistics.

Medusa provides all essential commerce features out-of-the-box. Businesses can go live and start selling without making any adjustments.

Businesses can also power-up their store by integrating third-party services for payments, fulfillment, and more.

[How Tekla created an ecommerce store using Medusa](https://medusajs.com/blog/tekla/).

***

## Install Ecommerce Store Powered by Medusa

You can use the following command to install an ecommerce store with Medusa:

```bash
npx create-medusa-app@latest --with-nextjs-starter
```

This installs:

- The Medusa application, which is composed of a Node.js server and a Medusa Admin dashboard. The dashboard opens right after the installation finishes, where you can create a user and start managing your store's data.
- A [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) that connects to your Medusa application to provide customers with ecommerce features.

***

## Integrate Third-Party Services

You can integrate third-party services and tools, customizing the architecture and commerce features of your store.

For example, you can integrate [Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) to accept payments, or [SendGrid](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md) to send emails to customers.

[Integrations](https://docs.medusajs.com/integrations/index.html.md): Check out available integrations for your Medusa application.

***

## Deploy the Medusa Application

The most efficient way to deploy your Medusa application is to use [Cloud](https://docs.medusajs.com/cloud/index.html.md). Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Cloud hosts your server, Admin dashboard, database, and Redis instance.

With Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub:

- Push to deploy.
- Multiple testing environments.
- Preview environments for new PRs.
- Test on production-like data.

Our documentation also provides a step-by-step guides to deploy your Medusa application and the Next.js Starter Storefront.

- [Sign up for Cloud](https://docs.medusajs.com/cloud/index.html.md): Learn more about Cloud and sign up to get started.
- [Deployment Guides](https://docs.medusajs.com/deployment/index.html.md): Learn how to deploy the Medusa application and Next.js Starter Storefront.

***

## Add Custom Features

Along with the extensive ecommerce features, Medusa also provides the architecture and Framework to customize your application and build custom features that are tailored for your business use case.

To learn how to develop customizations with Medusa, refer to the [Get Started](https://docs.medusajs.com/docs/learn/index.html.md) documentation.


# Integrate Odoo with Medusa

In this guide, you will learn how to implement the integration layer between Odoo and Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform that supports customizations. However, your business might already be using other systems such as an ERP to centralize data and processes. The Medusa Framework facilitates integrating the ERP system and using its data to enrich your commerce platform.

Odoo is a suite of open-source business apps that covers all your business needs, including an ERP system. You can use Odoo to store products and their prices, manage orders, and more.

This guide will teach you how to implement the general integration between Medusa and Odoo. You will learn how to connect to Odoo's APIs and fetch data such as products. You can then expand on this integration to implement your business requirements. You can also refer to [this recipe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/erp/index.html.md) to find general examples of ERP integration use cases and how to implement them.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You will first be asked for the project's name. You can also optionally choose to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).

Afterward, the installation process will start, installing the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credential and submit the form. Afterwards, you can login with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Install JSONRPC Package

[Odoo's APIs](https://www.odoo.com/documentation/18.0/developer/reference/external_api.html) are based on the XML-RPC and JSON-RPC protocols. So, to connect to Odoo's APIs, you need a JSON-RPC client library.

Run the following command in the Medusa application to install the `json-rpc-2.0` package:

```bash npm2yarn
npm install json-rpc-2.0
```

You will use this package in the next steps to connect to Odoo's APIs.

***

## Step 3: Create Odoo Module

To integrate third-party systems into Medusa, you create a custom [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a re-usable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

In this step, you'll create an Odoo Module that provides the interface to connect to and interact with Odoo. You will later use this module when implementing the product syncing logic.

Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).

### Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/odoo`.

![Diagram showcasing directory structure after creating the module directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1740474975/Medusa%20Resources/odoo-1_en3bso.jpg)

### Create Service

You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, which is useful if your module defines tables in the database, or connect to a third-party service.

Medusa registers the module's service in the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), allowing you to easily resolve the service from other customizations and use its methods.

The Medusa application registers resources, such as a module's service or the [logging tool](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md), in the Medusa container so that you can resolve them from other customizations, as you'll see in later sections. Learn more about it in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

In this section, you'll create the Odoo Module's service and the methods necessary to connect to Odoo.

To create the service, create the file `src/modules/odoo/service.ts` with the following content:

![Diagram showcasing directory structure after creating the service file](https://res.cloudinary.com/dza7lstvk/image/upload/v1740474976/Medusa%20Resources/odoo-2_y76dfs.jpg)

```ts title="src/modules/odoo/service.ts"
import { JSONRPCClient } from "json-rpc-2.0"

type Options = {
  url: string
  dbName: string
  username: string
  apiKey: string
}

export default class OdooModuleService {
  private options: Options
  private client: JSONRPCClient

  constructor({}, options: Options) {
    this.options = options

    this.client = new JSONRPCClient((jsonRPCRequest) => {
      fetch(`${options.url}/jsonrpc`, {
        method: "POST",
        headers: {
          "content-type": "application/json",
        },
        body: JSON.stringify(jsonRPCRequest),
      }).then((response) => {
        if (response.status === 200) {
          // Use client.receive when you received a JSON-RPC response.
          return response
            .json()
            .then((jsonRPCResponse) => this.client.receive(jsonRPCResponse))
        } else if (jsonRPCRequest.id !== undefined) {
          return Promise.reject(new Error(response.statusText))
        }
      })
    })
  }
}
```

You create an `OdooModuleService` class that has two class properties:

1. `options`: An object that holds the Odoo Module's options. Those include the API key, URL, database name, and username. You'll learn how to pass those to the module later.
2. `client`: An instance of the `JSONRPCClient` class from the `json-rpc-2.0` package. You'll use this client to connect to Odoo's APIs.

The service's constructor accepts as a second parameter the module's options. So, you use those to initialize the `options` property and create the `client` property. The `client` property is initialized with a function that sends a JSON-RPC request to Odoo's API and receives the response.

Next, you will add the methods to log in and fetch data from Odoo.

### Login Method

Before sending any request to Odoo's APIs, you need to have an authenticated UID of the user. So, you'll implement a method to retrieve that UID when it's not set.

Start by adding a `uid` property to the `OdooModuleService` class:

```ts title="src/modules/odoo/service.ts"
export default class OdooModuleService {
  private uid?: number
  // ...
}
```

Then, add the following `login` method:

```ts title="src/modules/odoo/service.ts"
export default class OdooModuleService {
  // ...
  async login() {
    this.uid = await this.client.request("call", {
      service: "common",
      method: "authenticate",
      args: [
        this.options.dbName, 
        this.options.username, 
        this.options.apiKey, 
        {},
      ],
    })
  }
}
```

The `login` method sends a JSON-RPC request to [Odoo's API to authenticate the user](https://www.odoo.com/documentation/18.0/developer/reference/external_api.html#logging-in). It uses the `client` property to send a request with the `service`, `method`, and `args` properties.

If the authentication was successful, Odoo returns a UID, which you store in the `uid` property.

### Fetch Products Method

You can fetch many data from Odoo based on your business requirements, or create data in Odoo. For this guide, you'll only learn how to fetch products. You will use this method later to sync products from Odoo to Medusa.

First, add the following types to `src/modules/odoo/service.ts`:

```ts title="src/modules/odoo/service.ts"
export type Pagination = {
  offset?: number
  limit?: number
}

export type OdooProduct = {
  id: number
  display_name: string
  is_published: boolean
  website_url: string
  name: string
  list_price: number
  description: string | false
  description_sale: string | false
  product_variant_ids: OdooProductVariant[]
  qty_available: number
  location_id: number | false
  taxes_id: number[]
  hs_code: string | false
  allow_out_of_stock_order: boolean
  is_kits: boolean
  image_1920: string
  image_1024: string
  image_512: string
  image_256: string
  image_128: string
  attribute_line_ids: {
    attribute_id: {
      display_name: string
    }
    value_ids: {
      display_name: string
    }[]
  }[]
  currency_id: {
    id: number
    display_name: string
  }
}

export type OdooProductVariant = Omit<
  OdooProduct, 
  "product_variant_ids" | "attribute_line_ids"
> & {
  product_template_variant_value_ids: {
    id: number
    name: string
    attribute_id: {
      display_name: string
    }
  }[]
  code: string
}
```

You define the following types:

- `Pagination`: An object that holds the pagination options for fetching products.
- `OdooProduct`: An object that represents an Odoo product. You define the properties that you'll fetch from Odoo's API. You can add more properties based on your business requirements.
- `OdooProductVariant`: An object that represents an Odoo product variant. You define the properties that you'll fetch from Odoo's API. You can add more properties based on your business requirements.

Then, add the following `listProducts` method to the `OdooModuleService` class:

```ts title="src/modules/odoo/service.ts"
export default class OdooModuleService {
  // ...
  async listProducts(filters?: any, pagination?: Pagination) {
    if (!this.uid) {
      await this.login()
    }

    const { offset, limit } = pagination || { offset: 0, limit: 10 }

    const ids = await this.client.request("call", {
      service: "object",
      method: "execute_kw",
      args: [
        this.options.dbName, 
        this.uid, 
        this.options.apiKey, 
        "product.template", 
        "search", 
        filters || [[
          ["is_product_variant", "=", false],
        ]], {
          offset,
          limit,
        },
      ],
    })
    
    // TODO retrieve product details based on ids
  }
}
```

In the `listProducts` method, you first check if the user is authenticated, and call the `login` method otherwise. Then, you send a JSON-RPC request to retrieve product IDs from Odoo with pagination and filter options. Odoo's APIs require you to first retrieve the IDs of the products and then fetch the details of each product.

To retrieve the products, replace the `TODO` with the following:

```ts title="src/modules/odoo/service.ts"
// product fields to retrieve
const productSpecifications = {
  id: {},
  display_name: {},
  is_published: {},
  website_url: {},
  name: {},
  list_price: {},
  description: {},
  description_sale: {},
  qty_available: {},
  location_id: {},
  taxes_id: {},
  hs_code: {},
  allow_out_of_stock_order: {},
  is_kits: {},
  image_1920: {},
  image_1024: {},
  image_512: {},
  image_256: {},
  currency_id: {
    fields: {
      display_name: {},
    },
  },
}

// retrieve products
const products: OdooProduct[] = await this.client.request("call", {
  service: "object",
  method: "execute_kw",
  args: [
    this.options.dbName, 
    this.uid, 
    this.options.apiKey, 
    type, 
    "web_read", 
    [ids], 
    {
      specification: {
        ...productSpecifications,
        product_variant_ids: {
          fields: {
            ...productSpecifications,
            product_template_variant_value_ids: {
              fields: {
                name: {},
                attribute_id: {
                  fields: {
                    display_name: {},
                  },
                },
              },
              context: {
                show_attribute: false,
              },
            },
            code: {},
          },
          context: {
            show_code: false,
          },
        },
        attribute_line_ids: {
          fields: {
            attribute_id: {
              fields: {
                display_name: {},
              },
            },
            value_ids: {
              fields: {
                display_name: {},
              },
              context: {
                show_attribute: false,
              },
            },
          },
        },
      },
    },
  ],
})

return products
```

You first define the `productSpecifications` object that holds the fields you want to fetch for each product and its variants. So, if you want to add more fields, you can add them in this object.

Then, you send a request to Odoo to fetch the products' details based on the IDs you retrieved earlier. You use the `productSpecifications` object to define the fields you want to fetch for each product and its variants. Finally, you return the fetched products.

You will use the `listProducts` method to sync products from Odoo to Medusa in the next steps.

### Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/odoo/index.ts` with the following content:

![Diagram showcasing directory structure after creating the module definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1740475883/Medusa%20Resources/odoo-3_q5y803.jpg)

```ts title="src/modules/odoo/index.ts"
import OdooModuleService from "./service"
import { Module } from "@medusajs/framework/utils"

export const ODOO_MODULE = "odoo"

export default Module(ODOO_MODULE, {
  service: OdooModuleService,
})
```

You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `odoo`.
2. An object with a required property `service` indicating the module's service.

### Add Module to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/odoo",
      options: {
        url: process.env.ODOO_URL,
        dbName: process.env.ODOO_DB_NAME,
        username: process.env.ODOO_USERNAME,
        apiKey: process.env.ODOO_API_KEY,
      },
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name. Modules that accept options also have an `options` property. You pass the options you defined in the `OdooModuleService` class to the module.

Then, set the environment variables in the `.env` file or system environment variables:

```plain
ODOO_URL=https://medusa8.odoo.com
ODOO_DB_NAME=medusa8
ODOO_USERNAME=test@gmail.com # username or email
ODOO_API_KEY=12345...
```

Where:

- `ODOO_URL`: The URL of your Odoo instance, which is of the format `https://<domain>.odoo.com`.
- `ODOO_DB_NAME`: The name of the database in your Odoo instance, which is the same as the domain in the URL.
- `ODOO_USERNAME`: The username or email of an Odoo user.
- `ODOO_API_KEY`: The API key of an Odoo user, or the user's password. To retrieve an API Key:
  - On your Odoo dashboard, click on the user's avatar at the top right and choose "My Profile" from the dropdown.

![My profile dropdown in Odoo](https://res.cloudinary.com/dza7lstvk/image/upload/v1740476295/Medusa%20Resources/Screenshot_2025-02-25_at_11.36.23_AM_h9iind.png)

- On your profile's page, click the "Account Security" tab, then the "New API Key" button.

![Profile page in Odoo](https://res.cloudinary.com/dza7lstvk/image/upload/v1740476296/Medusa%20Resources/Screenshot_2025-02-25_at_11.36.55_AM_gl4wfu.png)

- In the pop-up that opens, enter your password.
- Enter the API Key's name, and set the expiration to "Persistent Key", then click the "Generate Key" button.

![Generate key pop-up](https://res.cloudinary.com/dza7lstvk/image/upload/v1740476443/Medusa%20Resources/Screenshot_2025-02-25_at_11.40.19_AM_ntbx1c.png)

- Copy the generated API Key and use it as the `ODOO_API_KEY` environment variable's value.

You will test that the Odoo Module works as expected in the next steps.

***

## Step 4: Sync Products from Odoo to Medusa

There are different use cases you can implement when integrating an ERP like Odoo. One of them is syncing products from the ERP to Medusa. This way, you can manage products in Odoo and have them reflected in your commerce platform.

To implement the syncing functionality, you need to create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow similar to how you create a JavaScript function, but with additional features like defining rollback logic for each step, performing long actions asynchronously, and tracking the progress of the steps.

After defining the workflow, you can execute it in other customizations, such as periodically or when an event occurs.

In this section, you'll create a workflow that syncs products from Odoo to Medusa. Then, you'll execute that workflow once a day using a [scheduled job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md). The workflow has the following steps:

- [getProductsFromErp](#getProductsFromErp): Fetch products from Odoo
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Get Medusa store configurations to use when creating the products.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Get Medusa sales channels to use when creating the products.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Get Medusa shipping profiles to use when creating the products.
- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md): Create new products in Medusa.
- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md): Update existing products in Medusa.

The only step you'll need to implement is the `getProductsFromErp` step. The other steps are available through Medusa's `@medusajs/medusa/core-flows` package.

### getProductsFromErp

The first step of the workflow is to retrieve the products from the ERP. So, create the file `src/workflows/sync-from-erp.ts` with the following content:

![Diagram showcasing directory structure after creating the workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1740479240/Medusa%20Resources/odoo-4_jrh5mx.jpg)

```ts title="src/workflows/sync-from-erp.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

type Input = {
  offset: number
  limit: number
}

const getProductsFromErp = createStep(
  "get-products-from-erp",
  async (input: Input, { container }) => {
    const odooModuleService = container.resolve("odoo")

    const products = await odooModuleService.listProducts(undefined, input)

    return new StepResponse(products)
  }
)
```

You create a step using `createStep` from the Workflows SDK. It accepts two parameters:

1. The step's name, which is `get-products-from-erp`.
2. An async function that executes the step's logic. The function receives two parameters:
   - The input data for the step, which are the pagination fields `offset` and `limit`.
   - An object holding the workflow's context, including the [Medusa Container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) that allows you to resolve Framework and commerce tools.

In this step, you resolve the Odoo Module's service from the container and use its `listProducts` method to fetch products from Odoo. You pass the pagination options from the input data to the method.

A step must return an instance of `StepResponse` which accepts as a parameter the data to return, which is in this case the products.

### Create Workflow

You can now create the workflow that syncs the products from Odoo to Medusa.

In the same `src/workflows/sync-from-erp.ts` file, add the following imports:

```ts title="src/workflows/sync-from-erp.ts"
import { 
  createWorkflow, transform, WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  createProductsWorkflow, updateProductsWorkflow, useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { 
  CreateProductWorkflowInputDTO, UpdateProductWorkflowInputDTO,
} from "@medusajs/framework/types"
```

Then, add the workflow after the step:

```ts title="src/workflows/sync-from-erp.ts"
export const syncFromErpWorkflow = createWorkflow(
  "sync-from-erp",
  (input: Input) => {
    const odooProducts = getProductsFromErp(input)

    // @ts-ignore
    const { data: stores } = useQueryGraphStep({
      entity: "store",
      fields: [
        "default_sales_channel_id",
      ],
    })

    // @ts-ignore
    const { data: shippingProfiles } = useQueryGraphStep({
      entity: "shipping_profile",
      fields: ["id"],
      pagination: {
        take: 1,
      },
    }).config({ name: "shipping-profile" })

    const externalIdsFilters = transform({
      odooProducts,
    }, (data) => {
      return data.odooProducts.map((product) => `${product.id}`)
    })

    // @ts-ignore
    const { data: existingProducts } = useQueryGraphStep({
      entity: "product",
      fields: ["id", "external_id", "variants.*"],
      filters: {
        // @ts-ignore
        external_id: externalIdsFilters,
      },
    }).config({ name: "existing-products" })

    // TODO prepare products to create and update
  }
)
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function, which is the workflow's implementation. The function receives the pagination options as a parameter. In the workflow, you:

- Call the `getProductsFromErp` step to fetch products from Odoo.
- Use the [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md) to fetch the Medusa store configurations, sales channels, and shipping profiles. You'll use this data when creating the products in a later step.
  - The `useQueryGraphStep` uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which is a tool that retrieves data across modules.
- To figure out which products need to be updated, you retrieve products filtered by their `external_id` field, which you'll set to the Odoo product's ID when you create the products next.
  - Notice that you use `transform` from the Workflows SDK to create the external IDs filters. That's because data manipulation is not allowed in a workflow. You can learn more about this and other restrictions in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md).

Next, you need to prepare the products that should be created or updated. To do that, replace the `TODO` with the following:

```ts title="src/workflows/sync-from-erp.ts"
const {
  productsToCreate,
  productsToUpdate,
} = transform({
  existingProducts,
  odooProducts,
  shippingProfiles,
  stores,
}, (data) => {
  const productsToCreate: CreateProductWorkflowInputDTO[] = []
  const productsToUpdate: UpdateProductWorkflowInputDTO[] = []

  data.odooProducts.forEach((odooProduct) => {
    const product: CreateProductWorkflowInputDTO | UpdateProductWorkflowInputDTO = {
      external_id: `${odooProduct.id}`,
      title: odooProduct.display_name,
      description: odooProduct.description || odooProduct.description_sale || "",
      status: odooProduct.is_published ? "published" : "draft",
      options: odooProduct.attribute_line_ids.length ? odooProduct.attribute_line_ids.map((attribute) => {
        return {
          title: attribute.attribute_id.display_name,
          values: attribute.value_ids.map((value) => value.display_name),
        }
      }) : [
        {
          title: "Default",
          values: ["Default"],
        },
      ],
      hs_code: odooProduct.hs_code || "",
      handle: odooProduct.website_url.replace("/shop/", ""),
      variants: [],
      shipping_profile_id: data.shippingProfiles[0].id,
      sales_channels: [
        {
          id: data.stores[0].default_sales_channel_id || "",
        },
      ],
    }

    const existingProduct = data.existingProducts.find((p) => p.external_id === product.external_id)
    if (existingProduct) {
      product.id = existingProduct.id
    }

    if (odooProduct.product_variant_ids?.length) {
      product.variants = odooProduct.product_variant_ids.map((variant) => {
        const options = {}
        if (variant.product_template_variant_value_ids.length) {
          variant.product_template_variant_value_ids.forEach((value) => {
            options[value.attribute_id.display_name] = value.name
          })
        } else {
          product.options?.forEach((option) => {
            options[option.title] = option.values[0]
          })
        }
        return {
          id: existingProduct ? existingProduct.variants.find((v) => v.sku === variant.code)?.id : undefined,
          title: variant.display_name.replace(`[${variant.code}] `, ""),
          sku: variant.code || undefined,
          options,
          prices: [
            {
              amount: variant.list_price,
              currency_code: variant.currency_id.display_name.toLowerCase(),
            },
          ],
          manage_inventory: false, // change to true if syncing inventory from Odoo
          metadata: {
            external_id: `${variant.id}`,
          },
        }
      })
    } else {
      product.variants?.push({
        id: existingProduct ? existingProduct.variants[0].id : undefined,
        title: "Default",
        options: {
          Default: "Default",
        },
        // @ts-ignore
        prices: [
          {
            amount: odooProduct.list_price,
            currency_code: odooProduct.currency_id.display_name.toLowerCase(),
          },
        ],
        metadata: {
          external_id: `${odooProduct.id}`,
        },
        manage_inventory: false, // change to true if syncing inventory from Odoo
      })
    }

    if (existingProduct) {
      productsToUpdate.push(product as UpdateProductWorkflowInputDTO)
    } else {
      productsToCreate.push(product as CreateProductWorkflowInputDTO)
    }
  })

  return {
    productsToCreate,
    productsToUpdate,
  }
})

// TODO create and update the products
```

You use `transform` again to prepare the products to create and update. It receives two parameters:

- An object with the data you'll use in the transform function.
- The transform function, which receives the object from the first parameter, and returns the data that can be used in the rest of the workflow.

In the transform function, you:

- Create the `productsToCreate` and `productsToUpdate` arrays to hold the products that should be created and updated, respectively.
- Iterate over the products fetched from Odoo and create a product object for each. You set the product's properties based on the Odoo product's properties. If you want to add more properties, you can do so at this point.
  - Most importantly, you set the `external_id` to the Odoo product's ID, which allows you later to identify the product later when updating it or for other operations.
  - You also set the product's variants either to Odoo's variants or to a default variant. You set the product variant's Odoo ID in the `metadata.external_id` field, which allows you to identify the variant later when updating it or for other operations.
- To determine if a product already exists, you check if the product's `external_id` matches an existing product's `external_id`. You add it to the products to be updated. You apply a similar logic for the variants.
- Finally, you return an object with the `productsToCreate` and `productsToUpdate` arrays.

You can now create and update the products in the workflow. Replace the `TODO` with the following:

```ts title="src/workflows/sync-from-erp.ts"
createProductsWorkflow.runAsStep({
  input: {
    products: productsToCreate,
  },
})

updateProductsWorkflow.runAsStep({
  input: {
    products: productsToUpdate,
  },
})

return new WorkflowResponse({
  odooProducts,
})
```

You use the `createProductsWorkflow` and `updateProductsWorkflow` to create and update the products returned from the transform function. Since both of these are workflows, you use the `runAsStep` method to run them as steps in the current workflow.

Finally, a workflow must return a instance of `WorkflowResponse` passing it as a parameter the data to return, which in this case is the products fetched from Odoo.

You can now execute this workflow in other customizations, such as a scheduled job.

### Create Scheduled Job

In Medusa, you can run a task at a specified interval using a [scheduled job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md). A scheduled job is an asynchronous function that runs at a regular interval during the Medusa application's runtime to perform tasks such as syncing products from Odoo to Medusa.

To create a scheduled job, create the file `src/jobs/sync-products-from-erp.ts` with the following content:

![Diagram showcasing directory structure after creating the scheduled job file](https://res.cloudinary.com/dza7lstvk/image/upload/v1740480934/Medusa%20Resources/odoo-5_xf0xug.jpg)

```ts title="src/jobs/sync-products-from-erp.ts"
import {
  MedusaContainer,
} from "@medusajs/framework/types"
import { syncFromErpWorkflow } from "../workflows/sync-from-erp"
import { OdooProduct } from "../modules/odoo/service"

export default async function syncProductsJob(container: MedusaContainer) {
  const limit = 10
  let offset = 0
  let total = 0
  let odooProducts: OdooProduct[] = []

  console.log("Syncing products...")

  do {
    odooProducts = (await syncFromErpWorkflow(container).run({
      input: {
        limit,
        offset,
      },
    })).result.odooProducts

    offset += limit
    total += odooProducts.length
  } while (odooProducts.length > 0)

  console.log(`Synced ${total} products`)
}

export const config = {
  name: "daily-product-sync",
  schedule: "0 0 * * *", // Every day at midnight
}
```

In this file, you export:

- An asynchronous function, which is the task to execute at the specified schedule.
- A configuration object having the following properties:
  - `name`: A unique name for the scheduled job.
  - `schedule`: A [cron expression](https://crontab.guru/) string indicating the schedule to run the job at. The specified schedule indicates that this job should run every day at midnight.

The scheduled job function accepts the Medusa container as a parameter. In the function, you define the pagination options for the products to fetch from Odoo. You then run the `syncFromErpWorkflow` workflow with the pagination options. You increment the offset by the limit each time you run the workflow until you fetch all the products.

***

## Test it Out

To test out syncing the products from Odoo to Medusa, first, change the schedule of the job in `src/jobs/sync-products-from-erp.ts` to run every minute:

```ts title="src/jobs/sync-products-from-erp.ts"
export const config = {
  name: "daily-product-sync",
  schedule: "* * * * *", // Every minute
}
```

Then, start the Medusa application with the following command:

```bash npm2yarn
npm run dev
```

A minute later, you should find the message `Syncing products...` in the console. Once the job finishes, you should see the message `Synced <number> products`, indicating the number of products synced.

You can also confirm that the products were synced by checking the products in the Medusa Admin dashboard.

If you encounter any issues, make sure the module options are set correctly as explained in [this section](#add-module-to-medusas-configurations).

***

## Next Steps

You now have the foundation for integrating Odoo with Medusa. You can expand on this integration to implement more use cases, such as syncing orders, restricting purchases of products based on custom rules, and checking inventory in Odoo before adding to the cart. You can find the approach to implement these use cases in [this recipe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/erp/index.html.md).

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Integrate ERP with Medusa

In this recipe, you'll learn about the general approach to integrating an ERP system with Medusa.

Businesses often rely on an ERP system to centralize their data and custom business rules and operations. This includes using the ERP to store special product prices, manage custom business rules that change how their customers make purchases, and handle the fulfillment and processing of orders.

When integrating the ERP system with other ecommerce platforms, you'll face complications maintaining data consistency across systems and customizing the platform's existing flows to accommodate the ERP system's data and operations. For example, the ecommerce platform may not support purchasing products with custom pricing or restricting certain products from purchase under certain conditions.

The Medusa Framework solves these challenges by giving you a durable execution engine to orchestrate operations through custom flows, and the flexibility to customize the platform's existing flows. You can wrap existing flows with custom logic, inject custom features into existing flows, and create new flows that interact with the ERP system and sync data between the two systems.

![ERP Integration Illustration](https://res.cloudinary.com/dza7lstvk/image/upload/v1740470820/Medusa%20Resources/erp-medusa-integration_pxjpcx.jpg)

In this recipe, you'll learn how to implement some common use cases when integrating an ERP system with Medusa. This includes how to purchase products with custom pricing, restrict products from purchase under conditions in the ERP, sync orders to the ERP, and more.

You can use the code snippets in the recipe as a starting point for your ERP integration, making changes as necessary for your use case. You can also implement other use cases using the same Medusa concepts.

***

## Prerequisite: Install Medusa

If you don't have a Medusa application yet, you can install it with the following command:

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. You can also optionally choose to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Integrate ERP in a Module

Before you start integrating the ERP system into existing or new flows in Medusa, you must build the integration layer that allows you to communicate with the ERP in your customizations.

In Medusa, you implement integrations or features around a single commerce domain in a [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a reusable package that can interact with the database or external APIs. The module integrates into your Medusa application without side effects to the existing setup.

So, you can create a module that exports a class called a service, and in that service, you implement the logic to connect to your ERP system, fetch data from it, and send data to it. The service may look like this:

```ts title="src/modules/erp/service.ts"
type Options = {
  apiKey: string
}

export default class ErpModuleService {
  private options: Options
  private client

  constructor({}, options: Options) {
    this.options = options
    // TODO initialize client that connects to ERP
  }

  async getProducts() {
    // assuming client has a method to fetch products
    return this.client.getProducts()
  }

  // TODO add more methods
}
```

You can then use the module's service in the custom flows and customizations that you'll see in later sections.

### How to Create Module

Refer to [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) on how to create a module. You can also refer to the [Odoo Integration guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/erp/odoo/index.html.md) as an example of how to build a module that integrates an ERP into Medusa.

The rest of this recipe assumes you have an ERP Module with some methods to retrieve products, prices, and other relevant data.

***

## Sync Products from ERP

If you store products in the ERP system, you want to sync them into Medusa to allow customers to purchase them. You may sync them once or periodically to keep the products in Medusa up-to-date with the ERP.

Syncing data between systems is a big challenge and it's often the pitfall of most ecommerce platforms, as you need to ensure data consistency and handle errors gracefully. Medusa solves this challenge by providing a durable execution engine to complete tasks that span multiple systems, allowing you to orchestrate your operations across systems in Medusa instead of managing it yourself.

Medusa's workflows are a series of queries and actions, called steps, that complete a task. You construct a workflow similar to how you create a JavaScript function, but with additional features like defining rollback logic for each step, performing long actions asynchronously, and tracking the progress of the steps. You can then use these workflows in other customizations, such as:

- [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) to allow clients to trigger the workflow's execution.
- [Subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) to trigger the workflow when an event occurs.
- [Scheduled jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md) to run the workflow periodically.

So, to sync products from the ERP system to Medusa, you can create a custom workflow that fetches the products from the ERP system and adds them to Medusa. Then, you can create a scheduled job that syncs the products once a day, for example.

### 1. Create Workflow to Sync Products

The workflow that syncs products from the ERP system to Medusa will have a step that fetches the product from the ERP, and another step that adds the product to Medusa.

For example, you can create the following workflow:

```ts title="src/workflows/sync-products.ts"
import { 
  createStep, createWorkflow, StepResponse, 
  transform, WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"

const getProductsFromErpStep = createStep(
  "get-products-from-erp",
  async (_, { container }) => {
    const erpModuleService = container.resolve("erp")

    const products = await erpModuleService.getProducts()

    return new StepResponse(products)
  }
)

export const syncFromErpWorkflow = createWorkflow(
  "sync-from-erp",
  () => {
    const erpProducts = getProductsFromErpStep()

    const productsToCreate = transform({
      erpProducts,
    }, (data) => {
      // TODO prepare ERP products to be created in Medusa
      return data.erpProducts.map((erpProduct) => {
        return {
          title: erpProduct.title,
          external_id: erpProduct.id,
          variants: erpProduct.variants.map((variant) => ({
            title: variant.title,
            metadata: {
              external_id: variant.id,
            },
          })),
          // other data...
        }
      })
    })

    createProductsWorkflow.runAsStep({
      input: {
        products: productsToCreate,
      },
    })

    return new WorkflowResponse({
      erpProducts,
    })
  }
)
```

In the above file, you first create a `getProductsFromErpStep` that resolves the ERP Module's service from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools, including your modules, that you can access in your customizations. You can then call the `getProducts` method in the ERP Module's service to fetch the products from the ERP and return them.

Then, you create a `syncFromErpWorkflow` that executes the `getProductsFromErpStep` to get the products from the ERP, then prepare the products to be created in Medusa. For example, you can set the product's title, and specify its ID in the ERP using the `external_id` field. Also, assuming the ERP products have variants, you can map the variants to Medusa's format, setting the variant's title and its ERP ID in the metadata.

Finally, you pass the products to be created to the `createProductsWorkflow`, which is a built-in Medusa workflow that creates products.

Learn more about creating workflows and steps in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

Find a detailed example of implementing this workflow in the [Odoo Integration guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/erp/odoo/index.html.md).

### 2. Create Scheduled Job to Sync Products

After creating a workflow, you can create a scheduled job that runs the workflow periodically to sync the products from the ERP to Medusa.

For example, you can create the following scheduled job:

```ts title="src/scheduled-jobs/sync-products.ts"
import {
  MedusaContainer,
} from "@medusajs/framework/types"
import { syncFromErpWorkflow } from "../workflows/sync-from-erp"

export default async function syncProductsJob(container: MedusaContainer) {
  await syncFromErpWorkflow(container).run({})
}

export const config = {
  name: "daily-product-sync",
  schedule: "0 0 * * *", // Every day at midnight
}
```

You create a scheduled job that runs once a day, executing the `syncFromErpWorkflow` to sync the products from the ERP to Medusa.

Learn more about creating scheduled jobs in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md).

***

## Retrieve Custom Prices from ERP

Consider you store products in an ERP system with fixed prices, or prices based on different conditions. You want to display these prices in the storefront and allow customers to purchase products with these prices. To do that, you need the mechanism to fetch the custom prices from the ERP system and add the product to the cart with the custom price.

To do that, you can build a custom workflow that uses the ERP Module to retrieve the custom price of a product variant, then add the product to the cart with that price.

You can also follow [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/examples/guides/custom-item-price/index.html.md) for a step-by-step guide on how to add items with custom prices to the cart.

### 1. Create Step to Get Variant Price

One of the steps in the custom add-to-cart workflow is to retrieve the custom price of the product variant from the ERP system. The step's implementation will differ based on the ERP system you're integrating. Here's a general implementation of how the step would look like:

```ts title="src/workflows/steps/get-erp-price.ts"
import { MedusaError } from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

export type GetVariantErpPriceStepInput = {
  variant_external_id: string
  currencyCode: string
  quantity: number
}

export const getVariantErpPriceStep = createStep(
  "get-variant-erp-price",
  async (input: GetVariantErpPriceStepInput, { container }) => {
    const { variant_external_id, currencyCode, quantity } = input

    const erpModuleService = container.resolve("erp")

    const price = await erpModuleService.getErpPrice(
      variant_external_id, 
      currencyCode
    )

    return new StepResponse(
      price * quantity
    )
  }
)
```

You create the step using the `createStep` from the Workflows SDK. The step has a function that receives the variant's ID in the ERP, the currency code, and the quantity as input.

The function then resolves the ERP Module's service from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) and, assuming you have a `getErpPrice` method in your ERP Module's service, you call it to retrieve that price, and return it multiplied by the quantity.

### 2. Create Custom Add-to-Cart Workflow

You can now create the custom workflow that uses the previous step to retrieve a variant's custom price from the ERP, then add it to the cart with that price.

For example, you can create the following workflow:

```ts title="src/workflows/add-custom-to-cart.ts"
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { addToCartWorkflow, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { getVariantErpPriceStep } from "./steps/get-erp-price"

type AddCustomToCartWorkflowInput = {
  cart_id: string
  item: {
    variant_id: string
    quantity: number
  }
}

export const addCustomToCartWorkflow = createWorkflow(
  "add-custom-to-cart",
  ({ cart_id, item }: AddCustomToCartWorkflowInput) => {
    // Retrieve the cart's currency code
    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      filters: { id: cart_id },
      fields: ["id", "currency_code"],
    })

    // Retrieve the variant's metadata to get its ERP ID
    const { data: variants } = useQueryGraphStep({
      entity: "variant",
      fields: [
        "id",
        "metadata",
      ],
      filters: {
        id: item.variant_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    }).config({ name: "retrieve-variant" })

    // get the variant's price from the ERP
    const price = getVariantErpPriceStep({
      variant_external_id: variants[0].metadata?.external_id as string,
      currencyCode: carts[0].currency_code,
      quantity: item.quantity,
    })

    // prepare to add the variant to the cart
    const itemToAdd = transform({
      item,
      price,
      variants,
    }, (data) => {
      return [{
        ...data.item,
        unit_price: data.price,
        metadata: data.variants[0].metadata,
      }]
    })
    
    // add the variant to the cart
    addToCartWorkflow.runAsStep({
      input: {
        items: itemToAdd,
        cart_id,
      },
    })
  }
)
```

In this workflow, you first fetch the details of the cart and the variant from the database using the [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md) that Medusa defines.

Then, you use the `getVariantErpPriceStep` you created to retrieve the price from the ERP. You pass it the variant's ERP ID, supposedly stored in the variant's metadata as shown in the [previous section](#1-create-workflow-to-sync-products), the cart's currency code, and the quantity of the item.

Finally, you prepare the item to be added to the cart, setting the unit price to the price you retrieved from the ERP. You then call the `addToCartWorkflow` to add the item to the cart.

### 3. Execute Workflow in API Route

To allow clients to add products to the cart with custom prices, you can create an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) that exposes the workflow's functionality. An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts.

For example, you can create the following API route:

```ts title="src/api/store/cart/[id]/custom-line-items/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
import { HttpTypes } from "@medusajs/framework/types"
import { addCustomToCartWorkflow } from "../../../../../workflows/add-custom-to-cart"

export const POST = async (
  req: MedusaRequest, 
  res: MedusaResponse
) => {
  const { id } = req.params
  const item = req.body

  await addCustomToCartWorkflow(req.scope)
    .run({
      input: {
        cart_id: id,
        item,
      },
    })

  res.status(200).json({ success: true })
}
```

In this API route, you receive the cart's ID from the route's parameters, and the item to be added to the cart from the request body. You then call the `addCustomToCartWorkflow` to add the item to the cart with the custom price.

Learn more about how to create an API route in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). You can also add request body validation as explained in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/validation/index.html.md).

***

## Restrict Purchase with Custom ERP Rules

Your ERP may store restrictions on who can purchase what products. For example, you may allow only some companies to purchase certain products.

Since Medusa implements the add-to-cart functionality within a workflow, it allows you to inject custom logic into the workflow using [workflow hooks](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md). A workflow hook is a point in a workflow where you can inject a step and perform a custom functionality.

So, to implement the use case of product-purchase restriction, you can use the `validate` hook of the `addToCartWorkflow` or the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) to check if the customer is allowed to purchase the product. For example:

```ts title="src/workflows/hooks/validate-purchase.ts"
import { MedusaError } from "@medusajs/framework/utils"
import { addToCartWorkflow } from "@medusajs/medusa/core-flows"

addToCartWorkflow.hooks.validate(
  async ({ input, cart }, { container }) => {
    const erpModuleService = container.resolve("erp")
    const productModuleService = container.resolve("product")
    const customerModuleService = container.resolve("customer")
    
    const customer = cart.customer_id ? await customerModuleService.retrieveCustomer(cart.customer_id) : undefined
    const productVariants = await productModuleService.listProductVariants({
      id: input.items.map((item) => item.variant_id).filter(Boolean) as string[],
    }, {
      relations: ["product"],
    })

    await Promise.all(
      productVariants.map(async (productVariant) => {
        if (!productVariant.product?.external_id) {
          // product isn't in ERP
          return
        }
  
        const isAllowed = await erpModuleService.canCompanyPurchaseProduct(
          productVariant.product.external_id,
          customer?.company_name || undefined
        )
  
        if (!isAllowed) {
          throw new MedusaError(
            MedusaError.Types.NOT_ALLOWED,
            `Company ${customer?.company_name || ""} is not allowed to purchase product ${productVariant.product.id}`
          )
        }
      })
    )
  }
)
```

You consume a hook using the workflow's `hooks` property. In the above example, you consume the `validate` hook of the `addToCartWorkflow` to inject a step.

In the step, you resolve the ERP Module's service, the Product Module's service, and the Customer Module's service from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). You then retrieve the cart's customer and the product variants to be added to the cart.

Then, for each product variant, you use a `canCompanyPurchaseProduct` method in the ERP Module's service that checks if the customer's company is allowed to purchase the product. If not, you throw a `MedusaError` with a message that the company is not allowed to purchase the product.

So, only customers who are allowed in the ERP system to purchase a product can add it to the cart.

Learn more about workflow hooks and how to consume them in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md).

***

## Two-Way Order Syncing

After a customer places an order in Medusa, you may want to sync the order to the ERP system where you handle its fulfillment and processing. However, you may also want to sync the order back to Medusa, where you handle customer-service related operations, such as returns and refunds.

As explained earlier, workflows facilitate the orchestration of operations across systems while maintaining data consistency. So, you can create two workflows:

1. A workflow that syncs the order from Medusa to the ERP system. You can execute this workflow in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that is triggered when an order is created in Medusa.
2. A workflow that syncs the order from the ERP system back to Medusa. Then, you can create a webhook listener in Medusa that executes the workflow, and use the webhook in the ERP system to send order updates to Medusa.

### 1. Sync Order to ERP

To sync the order from Medusa to the ERP system, you can create a custom workflow that sends the order's details from Medusa to the ERP system. For example:

```ts title="src/workflows/sync-order-to-erp.ts"
import { createStep, createWorkflow, StepResponse, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { updateOrderWorkflow, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { OrderDTO } from "@medusajs/framework/types"

type StepInput = {
  order: OrderDTO
}

export const syncOrderToErpStep = createStep(
  "sync-order-to-erp",
  async ({ order }: StepInput, { container }) => {
    const erpModuleService = container.resolve("erp")

    const erpOrderId = await erpModuleService.createOrder(order)

    return new StepResponse(erpOrderId, erpOrderId)
  },
  async (erpOrderId, { container }) => {
    if (!erpOrderId) {
      return
    }

    const erpModuleService = container.resolve("erp")
    await erpModuleService.deleteOrder(erpOrderId)
  }
)

type WorkflowInput = {
  order_id: string
}

export const syncOrderToErpWorkflow = createWorkflow(
  "sync-order-to-erp",
  ({ order_id }: WorkflowInput) => {
    // @ts-ignore
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "*",
        "shipping_address.*",
        "billing_address.*",
        "items.*",
      ],
      filters: {
        id: order_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const erpOrderId = syncOrderToErpStep({
      order: orders[0] as unknown as OrderDTO,
    })

    updateOrderWorkflow.runAsStep({
      input: {
        id: order_id,
        user_id: "",
        metadata: {
          external_id: erpOrderId,
        },
      },
    })

    return new WorkflowResponse(erpOrderId)
})
```

You first create a `syncOrderToErpStep` that receives an order's details, resolves the ERP Module's service from the Medusa container, and calls a `createOrder` method in the ERP Module's service that creates the order in the ERP.

Notice that you pass to `createStep` a third-parameter function. This is the compensation function that defines how to rollback changes. So, when an error occurs during the workflow's execution, the step deletes the order from the ERP system. This ensures that the data remains consistent across systems.

Learn more about the compensation function in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/compensation-function/index.html.md).

Then, you create a `syncOrderToErpWorkflow` that retrieves the order's details from the database using the `useQueryGraphStep`, then executes the `syncOrderToErpStep` to sync the order to the ERP system. Finally, you update the order in Medusa to set the ERP order's ID in the `metadata` field.

You can now use this workflow whenever an order is placed. To do that, you can create a subscriber that listens to the `order.created` event and executes the workflow:

```ts title="src/subscribers/sync-order-to-erp.ts"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { syncOrderToErpWorkflow } from "../workflows/sync-order-to-erp"

export default async function productCreateHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await syncOrderToErpWorkflow(container)
    .run({
      input: {
        order_id: data.id,
      },
    })

  console.log(`Order synced to ERP with id: ${result}`)
}

export const config: SubscriberConfig = {
  event: "order.placed",
}
```

You create a subscriber that listens to the `order.placed` event. When the event is triggered, the subscriber executes the `syncOrderToErpWorkflow` to sync the order to the ERP system.

Learn more about events and subscribers in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).

### 2. Sync Order from ERP to Medusa

To sync the order from the ERP system back to Medusa, create first the workflow that receives the updated order data and reflects them in Medusa:

```ts title="src/workflows/sync-order-from-erp.ts"
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { updateOrderWorkflow, useQueryGraphStep } from "@medusajs/medusa/core-flows"

type Input = {
  order_erp_data: any
}

export const syncOrderFromErpWorkflow = createWorkflow(
  "sync-order-from-erp",
  ({ order_erp_data }: Input) => {
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: ["*"],
      filters: {
        // @ts-ignore
        metadata: {
          external_id: order_erp_data.id,
        },
      },
    })

    const orderUpdateData = transform({
      order_erp_data,
      orders,
    }, (data) => {
      return {
        id: data.orders[0].id,
        user_id: "",
        status: data.order_erp_data.status,
      }
    })

    const order = updateOrderWorkflow.runAsStep({
      input: orderUpdateData,
    })

    return new WorkflowResponse(order)
  }
)
```

In the workflow, you retrieve the order from the database using the `useQueryGraphStep`, assuming that the ERP order's ID is stored in the order's metadata. Then, you prepare the order's data to be updated in Medusa, setting the order's status to the status you received from the ERP system. Finally, you update the order using the `updateOrderWorkflow`.

You can now create an API route that receives webhook updates from the ERP system and executes the workflow:

```ts title="src/api/erp/order-updates/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { syncOrderFromErpWorkflow } from "../../workflows/sync-order-from-erp"

export async function POST(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const webhookData = req.rawBody

  // TODO construct the order object from the webhook data
  
  // execute the workflow
  await syncOrderFromErpWorkflow(req.scope).run({
    input: {
      order_erp_data, // pass constructed order object
    },
  })
}
```

In the API route, you receive the raw webhook data from the request body. You can then construct the order object from the webhook data based on the ERP system's format.

Then, you call the `syncOrderFromErpWorkflow` to sync the order from the ERP system back to Medusa.

Finally, to ensure the webhook's raw data is received, you need to configure the middleware that runs before the route handler to preserve the raw body data. To do that, add the following middleware configuration in `src/api/middlewares.ts`:

```ts title="src/api/middlewares.ts"
import { defineMiddlewares } from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      method: ["POST"],
      bodyParser: { preserveRawBody: true },
      matcher: "/erp/order-updates",
    },
  ],
})
```

You configure the body parser of `POST` requests to the `/erp/order-updates` route to preserve the raw body data.

You can now receive webhook requests from your ERP system and sync the order data back to Medusa.

Learn more about middlewares in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

***

## Validate Inventory Availability in ERP

An ERP system often manages the inventory of products, with the ability to track the stock levels and availability of products. When a customer is purchasing a product through Medusa, you want to ensure that the product is available in the ERP system before allowing the purchase.

Similar to the [product-purchase restriction](#restrict-purchase-with-custom-erp-rules) use case, you can use the `validate` hook of the `addToCartWorkflow` or the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) to check the product's availability in the ERP system. For example:

```ts title="src/workflows/hooks/validate-inventory.ts"
import { MedusaError } from "@medusajs/framework/utils"
import { addToCartWorkflow } from "@medusajs/medusa/core-flows"

addToCartWorkflow.hooks.validate(
  async ({ input }, { container }) => {
    const erpModuleService = container.resolve("erp")
    const productModuleService = container.resolve("product")
    
    const productVariants = await productModuleService.listProductVariants({
      id: input.items.map((item) => item.variant_id).filter(Boolean) as string[],
    }, {
      relations: ["product"],
    })

    await Promise.all(
      productVariants.map(async (productVariant) => {
        const erpVariant = await erpModuleService.getQty(productVariant.metadata?.external_id)
        const item = input.items.find((item) => item.variant_id === productVariant.id)!

        if (erpVariant.qty_available < item.quantity && !erpVariant.allow_out_of_stock_order) {
          throw new MedusaError(
            MedusaError.Types.NOT_ALLOWED,
            `Not enough stock for product ${productVariant.product?.id}`
          )
        }
      })
    )
  }
)
```

You consume the `validate` hook of the `addToCartWorkflow` to inject a step function. In the step, you resolve the services of the ERP Module and the Product Module from the Medusa container. Then, you loop over the product variants to be added to the cart, and for each variant, you call a `getQty` method in the ERP Module's service to get the variant's quantity available.

If the available quantity in the ERP is less than the quantity to be added to the cart, and the ERP doesn't allow out-of-stock orders for the variant, you throw an error that the product is out of stock.

So, only products that have sufficient quantity in the ERP system can be added to the cart.

***

## Implement More Use Cases

The use cases covered in this guide are some common ERP integration scenarios that you can implement with Medusa. However, you can implement more use cases based on your ERP system's capabilities and your business requirements.

Refer to the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md) to learn more about Medusa's concepts and how to implement customizations. You can also use the feedback form at the end of this guide to suggest more use cases you'd like to see implemented.


# Marketplace Recipe: Restaurant-Delivery Example

In this guide, you'll learn how to build a restaurant-delivery marketplace platform, similar to Uber Eats, with Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with support for customizations. While Medusa doesn't provide marketeplace functionalities natively, it provides features that you can extend and a Framework to support all your customization needs to build a marketplace.

In this guide, you'll customize Medusa to build a restaurant-delivery platform with the following features:

1. Manage restaurants, each having admin users and products.
2. Manage drivers and allow them to handle the delivery of orders from restaurants to customers.
3. Real-time delivery handling and tracking, from the restaurant accepting the order to the driver delivering the order to the customer.

- [Example Repository](https://github.com/medusajs/examples/tree/main/restaurant-marketplace): Find the full code for this recipe example in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1724757329/OpenApi/Restaurant-Delivery-Marketplace_vxao2l.yml): Imported this OpenApi Specs file into tools like Postman.

This recipe is adapted from [Medusa Eats](https://github.com/medusajs/medusa-eats), which offers more implementation details including a custom storefront to place and track orders.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. You can also optionally choose to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credential and submit the form. Afterwards, you can login with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create a Restaurant Module

Medusa creates commerce features in modules. For example, product features and data models are created in the Product Module.

You also create custom commerce data models and features in custom modules. They're integrated into the Medusa application similar to Medusa's modules without side effects.

So, you'll create a restaurant module that holds the data models related to a restaurant and allows you to manage them.

Create the directory `src/modules/restaurant`.

### Create Restaurant Data Models

Create the file `src/modules/restaurant/models/restaurant.ts` with the following content:

```ts title="src/modules/restaurant/models/restaurant.ts"
import { model } from "@medusajs/framework/utils"
import { RestaurantAdmin } from "./restaurant-admin"

export const Restaurant = model.define("restaurant", {
  id: model
    .id()
    .primaryKey(),
  handle: model.text(),
  is_open: model.boolean().default(false),
  name: model.text(),
  description: model.text().nullable(),
  phone: model.text(),
  email: model.text(),
  address: model.text(),
  image_url: model.text().nullable(),
  admins: model.hasMany(() => RestaurantAdmin, {
    mappedBy: "restaurant",
  }),
})
```

This defines a `Restaurant` data model with properties like `is_open` to track whether a restaurant is open, and `address` to show the restaurant’s address.

It also has a relation to the `RestaurantAdmin` data model that you’ll define next.

Create the file `src/modules/restaurant/models/restaurant-admin.ts` with the following content:

```ts title="src/modules/restaurant/models/restaurant-admin.ts"
import { model } from "@medusajs/framework/utils"
import { Restaurant } from "./restaurant"

export const RestaurantAdmin = model.define("restaurant_admin", {
  id: model
    .id()
    .primaryKey(),
  first_name: model.text(),
  last_name: model.text(),
  email: model.text(),
  avatar_url: model.text().nullable(),
  restaurant: model.belongsTo(() => Restaurant, {
    mappedBy: "admins",
  }),
})
```

This defines a `RestaurantAdmin` data model, which belongs to a restaurant. It represents an admin that can manage a restaurant and its data.

### Create Main Service for Restaurant Module

Next, create the main service of the module at `src/modules/restaurant/service.ts` with the following content:

```ts title="src/modules/restaurant/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import { Restaurant } from "./models/restaurant"
import { RestaurantAdmin } from "./models/restaurant-admin"

class RestaurantModuleService extends MedusaService({
  Restaurant,
  RestaurantAdmin,
}) {}

export default RestaurantModuleService
```

The service extends the [service factory](https://docs.medusajs.com/docs/learn/fundamentals/modules/service-factory/index.html.md), which provides basic data-management features.

### Create Restaurant Module Definition

Then, create the file `src/modules/restaurant/index.ts` that holds the module definition:

```ts title="src/modules/restaurant/index.ts"
import Service from "./service"
import { Module } from "@medusajs/framework/utils"

export const RESTAURANT_MODULE = "restaurantModuleService"

export default Module(RESTAURANT_MODULE, {
  service: Service,
})
```

### Add Restaurant Module to Medusa Configuration

Finally, add the module to the list of modules in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/restaurant",
    },
  ],
})
```

### Further Reads

- [How to Create a Module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md)
- [How to Create a Data Model](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md)

***

## Step 3: Create a Delivery Module

In this step, you’ll create the Delivery Module that defines delivery-related data models.

Create the directory `src/modules/delivery`.

### Create Types

Before creating the data models,  create the file `src/modules/delivery/types/index.ts` with the following content:

```ts title="src/modules/delivery/types/index.ts"
export enum DeliveryStatus {
  PENDING = "pending",
  RESTAURANT_DECLINED = "restaurant_declined",
  RESTAURANT_ACCEPTED = "restaurant_accepted",
  PICKUP_CLAIMED = "pickup_claimed",
  RESTAURANT_PREPARING = "restaurant_preparing",
  READY_FOR_PICKUP = "ready_for_pickup",
  IN_TRANSIT = "in_transit",
  DELIVERED = "delivered",
}
```

This adds an enum that is used by the data models.

### Create Delivery Data Models

Create the file `src/modules/delivery/models/driver.ts` with the following content:

```ts title="src/modules/delivery/models/driver.ts"
import { model } from "@medusajs/framework/utils"
import { Delivery } from "./delivery"

export const Driver = model.define("driver", {
  id: model
    .id()
    .primaryKey(),
  first_name: model.text(),
  last_name: model.text(),
  email: model.text(),
  phone: model.text(),
  avatar_url: model.text().nullable(),
  deliveries: model.hasMany(() => Delivery, {
    mappedBy: "driver",
  }),
})

```

This defines a `Driver` data model with properties related to a driver user.

It has a relation to a `Delivery` data model that you’ll create next.

Create the file `src/modules/delivery/models/delivery.ts` with the following content:

```ts title="src/modules/delivery/models/delivery.ts"
import { model } from "@medusajs/framework/utils"
import { DeliveryStatus } from "../types/common"
import { Driver } from "./driver"

export const Delivery = model.define("delivery", {
  id: model
    .id()
    .primaryKey(),
  transaction_id: model.text().nullable(),
  delivery_status: model.enum(DeliveryStatus).default(DeliveryStatus.PENDING),
  eta: model.dateTime().nullable(),
  delivered_at: model.dateTime().nullable(),
  driver: model.belongsTo(() => Driver, {
    mappedBy: "deliveries",
  }).nullable(),
})

```

This defines a `Delivery` data model with notable properties including:

- `transaction_id`: The ID of the workflow transaction that’s handling this delivery. This makes it easier to track the workflow’s execution and update its status later.
- `delivery_status`: The current status of the delivery.

It also has a relation to the `Driver` data model, indicating the driver handling the delivery.

### Create Main Service for Delivery Module

Then, create the main service of the Delivery Module at `src/modules/delivery/service.ts` with the following content:

```ts title="src/modules/delivery/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import { Delivery } from "./models/delivery"
import { Driver } from "./models/driver"

class DeliveryModuleService extends MedusaService({
  Delivery,
  Driver,
}) {}

export default DeliveryModuleService
```

The service extends the [service factory](https://docs.medusajs.com/docs/learn/fundamentals/modules/service-factory/index.html.md), which provides basic data-management features.

### Create Delivery Module Definition

Next, create the file `src/modules/delivery/index.ts` holding the module’s definition:

```ts title="src/modules/delivery/index.ts"
import Service from "./service"
import { Module } from "@medusajs/framework/utils"

export const DELIVERY_MODULE = "deliveryModuleService"

export default Module(DELIVERY_MODULE, {
  service: Service,
})
```

### Add Delivery Module to Medusa Configuration

Finally, add the module to the list of modules in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/delivery",
    },
  ],
})

```

***

## Step 4: Define Links

Modules are isolated in Medusa, making them reusable, replaceable, and integrable in your application without side effects.

So, you can't have relations between data models in modules. Instead, you define a link between them.

Links are relations between data models of different modules that maintain the isolation between the modules.

In this step, you’ll define links between the Restaurant and Delivery modules, and other modules:

1. Link between the `Restaurant` model and the Product Module's `Product` model.
2. Link between the `Restaurant` model and the Delivery Module's `Delivery` model.
3. Link between the `Delivery` model and the Cart Module's `Cart` model.
4. Link between the `Delivery` model and the Order Module's `Order` model.

### Restaurant \<> Product Link

Create the file `src/links/restaurant-products.ts` with the following content:

```ts title="src/links/restaurant-products.ts"
import RestaurantModule from "../modules/restaurant"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  RestaurantModule.linkable.restaurant,
  {
    linkable: ProductModule.linkable.product.id,
    isList: true,
  }
)
```

This defines a link between the Restaurant Module’s `restaurant` data model and the Product Module’s `product` data model, indicating that a restaurant is associated with its products.

Since a restaurant has multiple products, `isList` is enabled on the product’s side.

### Restaurant \<> Delivery Link

Create the file `src/links/restaurant-delivery.ts` with the following content:

```ts title="src/links/restaurant-delivery.ts"
import RestaurantModule from "../modules/restaurant"
import DeliveryModule from "../modules/delivery"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  RestaurantModule.linkable.restaurant, 
  {
    linkable: DeliveryModule.linkable.delivery,
    isList: true,
  }
)

```

This defines a link between the Restaurant Module’s `restaurant` data model and the Delivery Module’s `delivery` data model, indicating that a restaurant is associated with the deliveries created for it.

Since a restaurant has multiple deliveries, `isList` is enabled on the delivery’s side.

### Delivery \<> Cart

Create the file `src/links/delivery-cart.ts` with the following content:

```ts title="src/links/delivery-cart.ts"
import DeliveryModule from "../modules/delivery"
import CartModule from "@medusajs/medusa/cart"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  DeliveryModule.linkable.delivery,
  CartModule.linkable.cart
)
```

This defines a link between the Delivery Module’s `delivery` data model and the Cart Module’s `cart` data model, indicating that delivery is associated with the cart it’s created from.

### Delivery \<> Order

Create the file `src/links/delivery-order.ts` with the following content:

```ts title="src/links/delivery-order.ts"
import DeliveryModule from "../modules/delivery"
import OrderModule from "@medusajs/medusa/order"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  DeliveryModule.linkable.delivery,
  OrderModule.linkable.order
)
```

This defines a link between the Delivery Module’s `delivery` data model and the Order Module’s `order` data model, indicating that a delivery is associated with the order created by the customer.

### Further Reads

- [How to Define Links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md)

***

## Step 5: Run Migrations and Sync Links

To create tables for the above data models in the database, start by generating the migrations for the Restaurant and Delivery Modules with the following commands:

```bash
npx medusa db:generate restaurantModuleService
npx medusa db:generate deliveryModuleService
```

This generates migrations in the `src/modules/restaurant/migrations` and `src/modules/delivery/migrations` directories.

Then, to reflect the migration and links in the database, run the following command:

```bash
npx medusa db:migrate
```

***

## Step 6: Create Restaurant API Route

To expose custom commerce features to frontend applications, such as the Medusa Admin dashboard or a storefront, you expose an endpoint by creating an API route.

In this step, you’ll create the API route used to create a restaurant. This route requires no authentication, as anyone can create a restaurant.

### Create Types

Before implementing the functionalities, you’ll create type files in the Restaurant Module useful in the next steps.

Create the file `src/modules/restaurant/types/index.ts` with the following content:

```ts title="src/modules/restaurant/types/index.ts"
import { InferTypeOf } from "@medusajs/framework/types"
import RestaurantModuleService from "../service"
import { Restaurant } from "../models/restaurant"

export type CreateRestaurant = Omit<
  InferTypeOf<typeof Restaurant>, "id" | "admins"
>
```

This adds a type used for inputs in creating a restaurant.

Since the `Restaurant` data model is a variable, use `InferTypeOf` to infer its type.

### Create Workflow

To implement the functionality of creating a restaurant, create a workflow and execute it in the API route.

The workflow only has one step that creates a restaurant.

To implement the step, create the file `src/workflows/restaurant/steps/create-restaurant.ts` with the following content:

```ts title="src/workflows/restaurant/steps/create-restaurant.ts" highlights={createRestaurantHighlight} collapsibleLines="1-7" expandMoreLabel="Show Imports"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { 
  CreateRestaurant,
} from "../../../modules/restaurant/types/mutations"
import { RESTAURANT_MODULE } from "../../../modules/restaurant"
import RestaurantModuleService from "../../../modules/restaurant/service"

export const createRestaurantStep = createStep(
  "create-restaurant-step",
  async function (data: CreateRestaurant, { container }) {
    const restaurantModuleService: RestaurantModuleService = container.resolve(
      RESTAURANT_MODULE
    )

    const restaurant = await restaurantModuleService.createRestaurants(data)

    return new StepResponse(restaurant, restaurant.id)
  },
  function (id: string, { container }) {
    const restaurantModuleService: RestaurantModuleService = container.resolve(
      RESTAURANT_MODULE
    )

    return restaurantModuleService.deleteRestaurants(id)
  }
)
```

This creates a step that creates a restaurant. The step’s compensation function, which executes if an error occurs, deletes the created restaurant.

Next, create the workflow at `src/workflows/restaurant/workflows/create-restaurant.ts`:

```ts title="src/workflows/restaurant/workflows/create-restaurant.ts"
import {
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { createRestaurantStep } from "../steps/create-restaurant"
import { CreateRestaurant } from "../../../modules/restaurant/types"

type WorkflowInput = {
  restaurant: CreateRestaurant;
};

export const createRestaurantWorkflow = createWorkflow(
  "create-restaurant-workflow",
  function (input: WorkflowInput) {
    const restaurant = createRestaurantStep(input.restaurant)

    return new WorkflowResponse(restaurant)
  }
)
```

The workflow executes the step and returns the created restaurant.

### Create Route

You’ll now create the API route that executes the workflow.

Start by creating the file `src/api/restaurants/validation-schemas.ts` that holds the schema to validate the request body:

```ts title="src/api/restaurants/validation-schemas.ts"
import { z } from "zod"

export const restaurantSchema = z.object({
  name: z.string(),
  handle: z.string(),
  address: z.string(),
  phone: z.string(),
  email: z.string(),
  image_url: z.string().optional(),
})
```

Then, create the file `src/api/restaurants/route.ts` with the following content:

```ts title="src/api/restaurants/route.ts" highlights={createRestaurantRouteHighlights} collapsibleLines="1-10" expandMoreLabel="Show Imports"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
import { 
  CreateRestaurant,
  } from "../../modules/restaurant/types/mutations"
import { 
  createRestaurantWorkflow,
  } from "../../workflows/restaurant/workflows/create-restaurant"
import { restaurantSchema } from "./validation-schemas"

export async function POST(req: MedusaRequest, res: MedusaResponse) {
  const validatedBody = restaurantSchema.parse(req.body) as CreateRestaurant

  if (!validatedBody) {
    return MedusaError.Types.INVALID_DATA
  }

  const { result: restaurant } = await createRestaurantWorkflow(req.scope)
	  .run({
	    input: {
	      restaurant: validatedBody,
	    },
	  })

  return res.status(200).json({ restaurant })
}
```

This creates a `POST` API route at `/restaurants`. It executes the `createRestaurantWorkflow` to create a restaurant and returns it in the response.

### Test it Out

To test the API route out, start the Medusa application:

```bash
npm run dev
```

Then, send a `POST` request to `/restaurants` :

```bash
curl -X POST 'http://localhost:9000/restaurants' \
-H 'Content-Type: application/json' \
--data-raw '{
    "name": "Acme",
    "handle": "acme",
    "address": "1st street",
    "phone": "1234567",
    "email": "acme@restaurant.com"
}'
```

The API route creates a restaurant and returns it.

If you’re calling this API route from a frontend client, make sure to set the [CORS middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/cors/index.html.md) on it since it’s not under the `/store` or `/admin` route prefixes.

### Further Reads

- [How to Create a Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md)
- [What is a Compensation Function](https://docs.medusajs.com/docs/learn/fundamentals/workflows/compensation-function/index.html.md)
- [How to Create an API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md)

***

## Step 7: List Restaurants API Route

In this step, you’ll create the API routes that retrieves a list of restaurants.

In the file `src/api/restaurants/route.ts` add the following API route:

```ts title="src/api/restaurants/route.ts"
// other imports...
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { 
  ContainerRegistrationKeys, 
  QueryContext,
} from "@medusajs/framework/utils"

// ...

export async function GET(req: MedusaRequest, res: MedusaResponse) {
  const { currency_code = "eur", ...queryFilters } = req.query

  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: restaurants } = await query.graph({
    entity: "restaurants",
    fields: [
      "id",
      "handle",
      "name",
      "address",
      "phone",
      "email",
      "image_url",
      "is_open",
      "products.*",
      "products.categories.*",
      "products.variants.*",
      "products.variants.calculated_price.*",
    ],
    filters: queryFilters,
    context: {
      products: {
        variants: {
          calculated_price: QueryContext({
            currency_code,
          }),
        },
      },
    },
  })

  return res.status(200).json({ restaurants })
}
```

This creates a `GET` API route at `/restaurants`. It uses Query to retrieve a restaurant, its products, and the product variant’s prices for a specified currency.

### Test it Out

To test this API route out, send a `GET` request to `/restaurants`:

```bash
curl 'http://localhost:9000/restaurants'
```

This returns the list of restaurants in the response.

### Further Reads

- [What is and how to use it](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md)
- [How to Retrieve Prices for Product Variants](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price/index.html.md)

***

## Step 8: Create User API Route

In this step, you’ll create the API route that creates a driver or a restaurant admin user.

Medusa provides an authentication flow that allows you to authenticate custom user types:

1. Use the `/auth/{actor_type}/{provider}/register` route to obtain an authentication token for registration. `{actor_type}` is the custom user type, such as `driver`, and `{provider}` is the provider used for authentication, such as `emailpass`.
2. Use a custom route to create the user. You pass in the request header the authentication token from the previous request to associate your custom user with the authentication identity created for it in the previous request.
3. After that, you can retrieve an authenticated token for the user using the `/auth/{actor_type}/provider` API route.

### Create Workflow

To implement and expose a feature that manipulates data, you create a workflow that uses services to implement the functionality, then create an API route that executes that workflow.

So, you'll start by implementing the functionality to create a user in a workflow. The workflow has two steps:

1. Create the user in the database. This user is either a restaurant admin or a driver. So, you'll create two separate steps for each user type.
2. Set the actor type of the user’s authentication identity (created by the `/auth/{actor_type}/{provider}/register` API route). For this step, you’ll use `setAuthAppMetadataStep` from Medusa's core workflows.

You'll start by implementing the steps to create a restaurant admin. Create the file \`

To implement the first step, create the file `src/workflows/user/steps/create-restaurant-admin.ts` with the following content:

```ts title="src/workflows/user/steps/create-restaurant-admin.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { RESTAURANT_MODULE } from "../../../modules/restaurant"
import RestaurantModuleService from "../../../modules/restaurant/service"

export type CreateRestaurantAdminInput = {
  restaurant_id: string;
  email: string;
  first_name: string;
  last_name: string;
};

export const createRestaurantAdminStep = createStep(
  "create-restaurant-admin-step",
  async (
    data: CreateRestaurantAdminInput,
    { container }
  ) => {
    const restaurantModuleService: RestaurantModuleService = container.resolve(
      RESTAURANT_MODULE
    )
    const restaurantAdmin = await restaurantModuleService.createRestaurantAdmins(
      data
    )

    return new StepResponse(restaurantAdmin, restaurantAdmin.id)
  },
  async (id, { container }) => {
    if (!id) {
      return
    }

    const restaurantModuleService: RestaurantModuleService = 
      container.resolve(RESTAURANT_MODULE)

    await restaurantModuleService.deleteRestaurantAdmins(id)
  }
)

```

This creates a step that accepts as input the data of the restaurant to create and its type. The step creates a restaurant admin and returns it.

In the compensation function, you delete the created restaurant admin if an error occurs.

Then, to implement the step that creates a driver, create the file `src/workflows/user/steps/create-driver.ts` with the following content:

```ts title="src/workflows/user/steps/create-driver.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { DELIVERY_MODULE } from "../../../modules/delivery"
import DeliveryModuleService from "../../../modules/delivery/service"

export type CreateDriverInput = {
  email: string;
  first_name: string;
  last_name: string;
  phone: string;
  avatar_url?: string;
};

export const createDriverStep = createStep(
  "create-driver-step",
  async (
    data: CreateDriverInput,
    { container }
  ) => {
    const deliveryModuleService: DeliveryModuleService = container.resolve(
      DELIVERY_MODULE
    )
    
    const driver = await deliveryModuleService.createDrivers(data)

    return new StepResponse(driver, driver.id)
  },
  async (id, { container }) => {
    if (!id) {
      return
    }

    const deliveryModuleService: DeliveryModuleService = container.resolve(
      DELIVERY_MODULE
    )

    await deliveryModuleService.deleteDrivers(id)
  }
)
```

This creates a step that accepts as input the data of the driver to create. The step creates a driver and returns it.

In the compensation function, you delete the created driver if an error occurs.

Next, create the workflow in the file `src/workflows/user/workflows/create-user.ts`:

```ts title="src/workflows/user/workflows/create-user.ts" collapsibleLines="1-13" expandButtonLabel="Show Imports"
import { setAuthAppMetadataStep } from "@medusajs/medusa/core-flows"
import {
  createWorkflow,
  transform,
  when,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { CreateDriverInput, createDriverStep } from "../steps/create-driver"
import { 
  CreateRestaurantAdminInput, 
  createRestaurantAdminStep,
} from "../steps/create-restaurant-admin"

export type CreateUserWorkflowInput = {
  user: (CreateRestaurantAdminInput | CreateDriverInput) & {
    actor_type: "restaurant" | "driver";
  };
  auth_identity_id: string;
};

export const createUserWorkflow = createWorkflow(
  "create-user-workflow",
  function (input: CreateUserWorkflowInput) {
    // TODO create user
  }
)
```

In this file, you create the necessary types and the workflow with a `TODO`.

Replace the `TODO` with the following:

```ts title="src/workflows/user/workflows/create-user.ts" highlights={createUserHighlights}
const restaurantUser = when(input, (input) => input.user.actor_type === "restaurant")
  .then(() => {
    return createRestaurantAdminStep(
      input.user as CreateRestaurantAdminInput
    )
  })

  const driverUser = when(input, (input) => input.user.actor_type === "driver")
  .then(() => {
    return createDriverStep(
      input.user as CreateDriverInput
    )
  })

const { user, authUserInput } = transform({ input, restaurantUser, driverUser }, (data) => {
  const user = data.restaurantUser || data.driverUser
  return {
    user,
    authUserInput: {
      authIdentityId: data.input.auth_identity_id,
      actorType: data.input.user.actor_type,
      value: user?.id || "",
    },
  }
})

setAuthAppMetadataStep(authUserInput)

return new WorkflowResponse({
  user,
})
```

In the workflow, you:

1. Create a restaurant admin if the actor type is `restaurant`.
2. Create a driver if the actor type is `driver`.
3. Use `transform` to create the input to be passed to the next step and return the created user, which is either a restaurant admin or a driver.
4. Use `setAuthAppMetadataStep` from Medusa's core workflows to update the authentication identity and associate it with the new user.
5. Return the created user.

### Create API Route

You’ll now create the API route to create a new user using the `createUserWorkflow`.

Start by creating the file `src/api/users/validation-schemas.ts` that holds the schema necessary to validate the request body:

```ts title="src/api/users/validation-schemas.ts"
import { z } from "zod"

export const createUserSchema = z
  .object({
    email: z.string().email(),
    first_name: z.string(),
    last_name: z.string(),
    phone: z.string(),
    avatar_url: z.string().optional(),
    restaurant_id: z.string().optional(),
    actor_type: z.ZodEnum.create(["restaurant", "driver"]),
  })
```

Then, create the file `src/api/users/route.ts` with the following content:

```ts title="src/api/users/route.ts" collapsibleLines="1-10" expandButtonLabel="Show Imports"
import { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import {
  createUserWorkflow,
  CreateUserWorkflowInput,
} from "../../workflows/user/workflows/create-user"
import { createUserSchema } from "./validation-schemas"

export const POST = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const { auth_identity_id } = req.auth_context

  const validatedBody = createUserSchema.parse(req.body)

  const { result } = await createUserWorkflow(req.scope).run({
    input: {
      user: validatedBody,
      auth_identity_id,
    } as CreateUserWorkflowInput,
  })

  res.status(200).json({ user: result.user })
}

```

This creates a `POST` API route at `/users` that creates a driver or a restaurant admin.

### Add Authentication Middleware

A middleware is executed when an HTTP request is received and before the route handler. It can be used to guard routes based on restrictions or authentication.

The `/users` API route must only be accessed with the authentication token in the header. So, you must add an authentication middleware on the route.

Create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import { 
  authenticate, 
  defineMiddlewares,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      method: ["POST"],
      matcher: "/users",
      middlewares: [
        authenticate(["driver", "restaurant"], "bearer", {
          allowUnregistered: true,
        }),
      ],
    },
  ],
})
```

This applies the `authenticate` middleware from the Medusa Framework on the `POST /users` API routes.

### Test it Out: Create Restaurant Admin

To create a restaurant admin:

1. Send a `POST` request to `/auth/restaurant/emailpass` to retrieve the token for the next request:

```bash
curl -X POST 'http://localhost:9000/auth/restaurant/emailpass/register' \
--data-raw '{
    "email": "admin@restaurant.com",
    "password": "supersecret"
}'
```

2. Send a `POST` request to `/users`, passing the token received from the previous request in the header:

```bash
curl -X POST 'http://localhost:9000/users' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data-raw '{
    "email": "admin@restaurant.com",
    "first_name": "admin",
    "last_name": "restaurant",
    "phone": "1234566",
    "actor_type": "restaurant",
    "restaurant_id": "res_01J5ZWMY48JWFY4W5Y8B3NER7S"
}' 
```

Notice that you must also pass the restaurant ID in the request body.

This returns the created restaurant admin user.

### Test it Out: Create Driver

To create a driver:

1. Send a `POST` request to `/auth/driver/emailpass` to retrieve the token for the next request:

```bash
curl -X POST 'http://localhost:9000/auth/driver/emailpass/register' \
-H 'Content-Type: application/json' \
--data-raw '{
    "email": "driver@gmail.com",
    "password": "supersecret"
}'
```

2. Send a `POST` request to `/users`, passing the token received from the previous request in the header:

```bash
curl --location 'http://localhost:9000/users' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data-raw '{
    "email": "driver@gmail.com",
    "first_name": "driver",
    "last_name": "test",
    "phone": "1234566",
    "actor_type": "driver"
}'
```

This returns the created driver user.

### Further Reads

- [How to Create an Actor Type](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md)

***

## Step 9: Delete Restaurant Admin API Route

In this step, you'll create a workflow that deletes the restaurant admin and its association to its auth identity, then use it in an API route.

The same logic can be applied to delete a driver.

### Create deleteRestaurantAdminStep

First, create the step that deletes the restaurant admin at `restaurant-marketplace/src/workflows/restaurant/steps/delete-restaurant-admin.ts`:

```ts title="restaurant-marketplace/src/workflows/restaurant/steps/delete-restaurant-admin.ts"
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { RESTAURANT_MODULE } from "../../../modules/restaurant"
import { DeleteRestaurantAdminWorkflow } from "../workflows/delete-restaurant-admin"
import RestaurantModuleService from "../../../modules/restaurant/service"

export const deleteRestaurantAdminStep = createStep(
  "delete-restaurant-admin",
  async ({ id }: DeleteRestaurantAdminWorkflow, { container }) => {
    const restaurantModuleService: RestaurantModuleService = container.resolve(
      RESTAURANT_MODULE
    )

    const admin = await restaurantModuleService.retrieveRestaurantAdmin(id)

    await restaurantModuleService.deleteRestaurantAdmins(id)
    
    return new StepResponse(undefined, { admin })
  },
  async (data, { container }) => {
    if (!data) {
      return
    }
    const restaurantModuleService: RestaurantModuleService = container.resolve(
      RESTAURANT_MODULE
    )

    const { restaurant: _, ...adminData } = data.admin

    await restaurantModuleService.createRestaurantAdmins(adminData)
  }
)
```

In this step, you resolve the Restaurant Module's service and delete the admin. In the compensation function, you create the admin again.

### Create deleteRestaurantAdminWorkflow

Then, create the workflow that deletes the restaurant admin at `restaurant-marketplace/src/workflows/restaurant/workflows/delete-restaurant-admin.ts`:

```ts title="restaurant-marketplace/src/workflows/restaurant/workflows/delete-restaurant-admin.ts" collapsibleLines="1-13" expandButtonLabel="Show Imports"
import { MedusaError } from "@medusajs/framework/utils"
import {
  WorkflowData,
  WorkflowResponse,
  createWorkflow,
  transform,
} from "@medusajs/framework/workflows-sdk"
import { 
  setAuthAppMetadataStep,
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { deleteRestaurantAdminStep } from "../steps/delete-restaurant-admin"

export type DeleteRestaurantAdminWorkflow = {
  id: string
}

export const deleteRestaurantAdminWorkflow = createWorkflow(
  "delete-restaurant-admin",
  (
    input: WorkflowData<DeleteRestaurantAdminWorkflow>
  ): WorkflowResponse<string> => {
    deleteRestaurantAdminStep(input)

    // TODO update auth identity
  }
)
```

So far, you only use the `deleteRestaurantAdminStep` in the workflow, which deletes the restaurant admin.

Replace the `TODO` with the following:

```ts title="restaurant-marketplace/src/workflows/restaurant/workflows/delete-restaurant-admin.ts"
const { data: authIdentities } = useQueryGraphStep({
  entity: "auth_identity",
  fields: ["id"],
  filters: {
    // @ts-ignore
    app_metadata: {
      restaurant_id: input.id,
    },
  },
})

const authIdentity = transform(
  { authIdentities },
  ({ authIdentities }) => {
    const authIdentity = authIdentities[0]

    if (!authIdentity) {
      throw new MedusaError(
        MedusaError.Types.NOT_FOUND,
        "Auth identity not found"
      )
    }

    return authIdentity
  }
)

setAuthAppMetadataStep({
  authIdentityId: authIdentity.id,
  actorType: "restaurant",
  value: null,
})

return new WorkflowResponse(input.id)
```

After deleting the restaurant admin, you:

1. Retrieve its auth identity using Query. To do that, you filter its `app_metadata` property by checking that its `restaurant_id` property's value is the admin's ID. For drivers, you replace `restaurant_id` with `driver_id`.
2. Check that the auth identity exists using `transform`. Otherwise, throw an error.
3. Unset the association between the auth identity and the restaurant admin using `setAuthAppMetadataStep` from Medusa's core workflows.

### Create API Route

Finally, add the API route that uses the workflow at `src/api/restaurants/[id]/admins/[admin_id]/route.ts`:

```ts title="src/api/restaurants/[id]/admins/[admin_id]/route.ts"
import {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { 
  deleteRestaurantAdminWorkflow,
} from "../../../../../workflows/restaurant/workflows/delete-restaurant-admin"

export const DELETE = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  await deleteRestaurantAdminWorkflow(req.scope).run({
    input: {
      id: req.params.admin_id,
    },
  })

  res.json({ message: "success" })
}
```

You add a `DELETE` API route at `/restaurants/[id]/admins/[admin_id]`. In the route, you execute the workflow to delete the restaurant admin.

### Add Authentication Middleware

This API route should only be accessible by restaurant admins.

So, in the file `src/api/middlewares.ts`, add a new middleware:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  routes: [
    // ...
    {
      method: ["POST", "DELETE"],
      matcher: "/restaurants/:id/**",
      middlewares: [
        authenticate(["restaurant", "user"], "bearer"),
      ],
    },
  ],
})
```

This allows only restaurant admins and Medusa Admin users to access routes under the `/restaurants/[id]` prefix if the request method is `POST` or `DELETE`.

### Test API Route

To test it out, create another restaurant admin user, then send a `DELETE` request to `/restaurants/[id]/admins/[admin_id]`, authenticated as the first admin user you created:

```bash
curl -X DELETE 'http://localhost:9000/restaurants/01J7GHGQTCAVY5C1AH1H733Q4G/admins/01J7GJKHWXF1YDMXH09EXEDCD6' \
-H 'Authorization: Bearer {token}'
```

Make sure to replace the first ID with the restaurant's ID, and the second ID with the ID of the admin to delete.

***

## Step 10: Create Restaurant Product API Route

In this step, you’ll create the API route that creates a product for a restaurant.

### Create Workflow

You’ll start by creating a workflow that creates the restaurant’s products. It has two steps:

1. Create the product using Medusa’s `createProductsWorkflow` as a step. This workflow is available through Medusa's core workflows.
2. Create a link between the restaurant and the products using `createRemoateLinkStep` from Medusa's core workflows.

So, create the workflow in the file `src/workflows/restaurant/workflows/create-restaurant-products.ts` with the following content:

```ts title="src/workflows/restaurant/workflows/create-restaurant-products.ts" highlights={createProductHighlights} collapsibleLines="13" expandButtonLabel="Show Imports"
import { 
  createProductsWorkflow,
  createRemoteLinkStep,
} from "@medusajs/medusa/core-flows"
import { CreateProductWorkflowInputDTO } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
import {
  WorkflowResponse,
  createWorkflow,
  transform,
} from "@medusajs/framework/workflows-sdk"
import { RESTAURANT_MODULE } from "../../../modules/restaurant"

type WorkflowInput = {
  products: CreateProductWorkflowInputDTO[];
  restaurant_id: string;
};

export const createRestaurantProductsWorkflow = createWorkflow(
  "create-restaurant-products-workflow",
  function (input: WorkflowInput) {
    const products = createProductsWorkflow.runAsStep({
      input: {
        products: input.products,
      },
    })

    const links = transform({
      products,
      input,
    }, (data) => data.products.map((product) => ({
      [RESTAURANT_MODULE]: {
        restaurant_id: data.input.restaurant_id,
      },
      [Modules.PRODUCT]: {
        product_id: product.id,
      },
    })))

    createRemoteLinkStep(links)

    return new WorkflowResponse(products)
  }
)

```

In the workflow, you:

1. Execute the `createProductsWorkflow` as a step, passing the workflow’s input as the details of the product.
2. Use `transform` to create a `links` object used to specify the links to create in the next step.
3. Use the `createRemoteLinkStep` to create the links between the restaurant and the products.
4. Return the created products.

### Create API Route

Create the file `src/api/restaurants/[id]/products/route.ts` with the following content:

```ts title="src/api/restaurants/[id]/products/route.ts" collapsibleLines="1-9" expandButtonLabel="Show Imports"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { 
  AdminCreateProduct,
} from "@medusajs/medusa/api/admin/products/validators"
import { z } from "zod"
import { 
  createRestaurantProductsWorkflow,
} from "../../../../workflows/restaurant/workflows/create-restaurant-products"

const createSchema = z.object({
  products: AdminCreateProduct().array(),
})

export async function POST(req: MedusaRequest, res: MedusaResponse) {
  const validatedBody = createSchema.parse(req.body)

  const { result: restaurantProducts } = await createRestaurantProductsWorkflow(
    req.scope
  ).run({
    input: {
      products: validatedBody.products as any[],
      restaurant_id: req.params.id,
    },
  })

  return res.status(200).json({ restaurant_products: restaurantProducts })
}
```

The creates a `POST` API route at `/restaurants/[id]/products`. It accepts the products’ details in the request body, executes the `createRestaurantProductsWorkflow` to create the products, and returns the created products in the response.

### Test it Out

To create a product using the above API route, send a `POST` request to `/restaurants/[id]/products`, replacing `[id]` with the restaurant’s ID:

```bash
curl -X POST 'http://localhost:9000/restaurants/res_01J5X704WQTFSZMRC7Z6S3YAC7/products' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data '{
    "products": [
        {
            "title": "Sushi",
            "status": "published",
            "variants": [
                {
                    "title": "Default",
                    "prices": [
                        {
                            "currency_code": "eur",
                            "amount": 20
                        }
                    ],
                    "manage_inventory": false,
                    "options": [
                      {
                        "title": "Default Title",
                        "values": ["Default Value"]
                      }
                    ]
                }
            ],
            "sales_channels": [
                {
                    
                    "id": "sc_01J5ZWK4MKJF85PM8KTW0BWMCK"
                }
            ]
        }
    ]
}'
```

Make sure to replace the sales channel’s ID with the ID of a sales channel in your store. This is necessary when you later create an order, as the cart must have the same sales channel as the product.

The request returns the created product in the response.

***

## Step 11: Create Order Delivery Workflow

In this step, you’ll create the workflow that creates a delivery. You’ll use it at a later step once a customer places their order.

The workflow to create a delivery has three steps:

1. `validateRestaurantStep` that checks whether a restaurant with the specified ID exists.
2. `createDeliveryStep` that creates the delivery.
3. `createRemoteLinkStep` that creates links between the different data model records. This step is from Medusa's core workflows.

### Create validateRestaurantStep

To create the first step, create the file `src/workflows/delivery/steps/validate-restaurant.ts` with the following content:

```ts title="src/workflows/delivery/steps/validate-restaurant.ts"
import { 
  createStep,
} from "@medusajs/framework/workflows-sdk"
import { RESTAURANT_MODULE } from "../../../modules/restaurant"
import RestaurantModuleService from "../../../modules/restaurant/service"

type ValidateRestaurantStepInput = {
  restaurant_id: string
}

export const validateRestaurantStep = createStep(
  "validate-restaurant",
  async ({ restaurant_id }: ValidateRestaurantStepInput, { container }) => {
    const restaurantModuleService: RestaurantModuleService = container.resolve(
      RESTAURANT_MODULE
    )

    // if a restaurant with the ID doesn't exist, an error is thrown
    await restaurantModuleService.retrieveRestaurant(
      restaurant_id
    )
  }
)
```

This step tries to retrieve the restaurant using the Restaurant Module’s main service. If the restaurant doesn’t exist, and error is thrown and the workflow stops execution.

### Create createDeliveryStep

Next, create the file `src/workflows/delivery/steps/create-delivery.ts` with the following content to create the second step:

```ts title="src/workflows/delivery/steps/create-delivery.ts" highlights={createDeliveryStepHighlights}
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { DELIVERY_MODULE } from "../../../modules/delivery"
import DeliveryModuleService from "../../../modules/delivery/service"

export const createDeliveryStep = createStep(
  "create-delivery-step",
  async function (_, { container }) {
    const deliverModuleService: DeliveryModuleService = 
      container.resolve(DELIVERY_MODULE)

    const delivery = await deliverModuleService.createDeliveries({})

    return new StepResponse(delivery, {
      delivery_id: delivery.id,
    })
  },
  async function (data, { container }) {
    if (!data) {
      return
    }
    const deliverModuleService: DeliveryModuleService = 
      container.resolve(DELIVERY_MODULE)

    deliverModuleService.softDeleteDeliveries(data.delivery_id)
  }
)

```

This step creates a delivery and returns it. In the compensation function, it deletes the delivery.

### Create createDeliveryWorkflow

Finally, create the workflow in `src/workflows/delivery/workflows/create-delivery.ts`:

```ts title="src/workflows/delivery/workflows/create-delivery.ts" highlights={createDeliveryWorkflowHighlights} collapsibleLines="1-13" expandButtonLabel="Show Imports"
import {
  WorkflowData,
  WorkflowResponse,
  createWorkflow,
  transform,
} from "@medusajs/framework/workflows-sdk"
import { LinkDefinition } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
import { DELIVERY_MODULE } from "../../../modules/delivery"
import { RESTAURANT_MODULE } from "../../../modules/restaurant"
import { validateRestaurantStep } from "../steps/validate-restaurant"
import { createDeliveryStep } from "../steps/create-delivery"

type WorkflowInput = {
  cart_id: string;
  restaurant_id: string;
};

export const createDeliveryWorkflowId = "create-delivery-workflow"
export const createDeliveryWorkflow = createWorkflow(
  createDeliveryWorkflowId,
  function (input: WorkflowInput) {
    validateRestaurantStep({
      restaurant_id: input.restaurant_id,
    })
    const delivery = createDeliveryStep()

    const links = transform({
      input,
      delivery,
    }, (data) => ([
      {
        [DELIVERY_MODULE]: {
          delivery_id: data.delivery.id,
        },
        [Modules.CART]: {
          cart_id: data.input.cart_id,
        },
      },
      {
        [RESTAURANT_MODULE]: {
          restaurant_id: data.input.restaurant_id,
        },
        [DELIVERY_MODULE]: {
          delivery_id: data.delivery.id,
        },
      },
    ] as LinkDefinition[]))

    createRemoteLinkStep(links)

    return new WorkflowResponse(delivery)
  }
)
```

In the workflow, you:

1. Use the `validateRestaurantStep` to validate that the restaurant exists.
2. Use the `createDeliveryStep` to create the delivery.
3. Use `transform` to specify the links to be created in the next step. You specify links between the delivery and cart, and between the restaurant and delivery.
4. Use the `createRemoteLinkStep` to create the links.
5. Return the created delivery.

***

## Step 12: Handle Delivery Workflow

In this step, you’ll create the workflow that handles the different stages of the delivery. This workflow needs to run in the background to update the delivery when an action occurs.

For example, when a restaurant finishes preparing the order’s items, this workflow creates a fulfillment for the order.

This workflow will be a [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md) that runs asynchronously in the background. Its async steps only succeed once an outside action sets its status, allowing the workflow to move to the next step.

API routes that perform actions related to the delivery, which you’ll create later, will trigger the workflow to move to the next step.

### Workflow’s Steps

The workflow has the following steps:

Steps that have a `*` next to their names are async steps.

- [setTransactionIdStep](#create-setTransactionIdStep): Sets the ID of the workflow’s transaction in the delivery’s \`transaction\_id\` property. This is useful for moving the steps of the
- [notifyRestaurantStep\*](#create-notifyRestaurantStep): An async step that omits an event to notify restaurants of the new order.
- [awaitDriverClaimStep\*](#create-awaitDriverClaimStep): An async step that’s executed once the restaurant accepts the order. It waits until a driver claims the delivery.
- [createOrderStep](#create-createOrderStep): Creates an order once a driver claims the delivery. It also returns links to be created by the next step.
- [createRemoteLinkStep](https://docs.medusajs.com/references/helper-steps/createRemoteLinkStep/index.html.md): Creates the links returned by the previous step between the order and delivery. This is from Medusa's core workflows.
- [awaitStartPreparationStep\*](#create-awaitStartPreparationStep): An async step that waits until the restaurant changes the delivery’s status to \`restaurant\_preparing\`.
- [awaitPreparationStep\*](#create-awaitPreparationStep): An async step that once the restaurant changes the delivery’s status to preparing, waits until the restaurant changes the delivery’s status to \`ready\_for\_pickup\`.
- [createFulfillmentStep](#create-createFulfillmentStep): Creates a fulfillment after the restaurant changes the delivery’s status to \`ready\_for\_pickup\`.
- [awaitPickUpStep\*](#create-awaitPickUpStep): An async step that waits until the driver changes the delivery’s status to \`in\_transit\`.
- [awaitDeliveryStep\*](#create-awaitDeliveryStep): An async step that waits until the driver changes the delivery’s status to \`delivered\`.

You’ll implement these steps next.

### create setTransactionIdStep

Create the file `src/workflows/delivery/steps/set-transaction-id.ts` with the following content:

```ts title="src/workflows/delivery/steps/set-transaction-id.ts" highlights={setTransactionIdStepHighlights}
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { DELIVERY_MODULE } from "../../../modules/delivery"
import DeliveryModuleService from "../../../modules/delivery/service"

export type SetTransactionIdStepInput = {
  delivery_id: string;
};

export const setTransactionIdStep = createStep(
  "create-delivery-step",
  async function (deliveryId: string, { container, context }) {
    const deliverModuleService: DeliveryModuleService = 
      container.resolve(DELIVERY_MODULE)

    const delivery = await deliverModuleService.updateDeliveries({
      id: deliveryId,
      transaction_id: context.transactionId,
    })

    return new StepResponse(delivery, delivery.id)
  },
  async function (delivery_id: string, { container }) {
    const deliverModuleService: DeliveryModuleService = 
      container.resolve(DELIVERY_MODULE)

    await deliverModuleService.updateDeliveries({
      id: delivery_id,
      transaction_id: null,
    })
  }
)
```

In this step, you update the `transaction_id` property of the delivery to the current workflow execution’s transaction ID. It can be found in the `context` property passed in the second object parameter of the step.

In the compensation function, you set the `transaction_id` to `null`.

### create notifyRestaurantStep

Create the file `src/workflows/delivery/steps/notify-restaurant.ts` with the following content:

```ts title="src/workflows/delivery/steps/notify-restaurant.ts" highlights={notifyRestaurantStepHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  Modules,
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk"

export const notifyRestaurantStepId = "notify-restaurant-step"
export const notifyRestaurantStep = createStep(
  {
    name: notifyRestaurantStepId,
    async: true,
    timeout: 60 * 15,
    maxRetries: 2,
  },
  async function (deliveryId: string, { container }) {
    const query = container.resolve(ContainerRegistrationKeys.QUERY)

    const { data: [delivery] } = await query.graph({
      entity: "deliveries",
      filters: {
        id: deliveryId,
      },
      fields: ["id", "restaurant.id"],
    })

    const eventBus = container.resolve(Modules.EVENT_BUS)

    await eventBus.emit({
      name: "notify.restaurant",
      data: {
        restaurant_id: delivery.restaurant?.id,
        delivery_id: delivery.id,
      },
    })
  }
)
```

In this step, you:

- Retrieve the delivery with its linked restaurant.
- Emit a `notify.restaurant` event using the event bus module’s service.

Since the step is async, the workflow only removes past it once it’s marked as successful, which will happen when the restaurant accepts the order.

A step is async if the `async` option is specified in the first object parameter of `createStep`.

### Create awaitDriverClaimStep

Create the file `src/workflows/delivery/steps/await-driver-claim.ts` with the following content:

```ts title="src/workflows/delivery/steps/await-driver-claim.ts"
import { createStep } from "@medusajs/framework/workflows-sdk"

export const awaitDriverClaimStepId = "await-driver-claim-step"
export const awaitDriverClaimStep = createStep(
  { 
    name: awaitDriverClaimStepId, 
    async: true, 
    timeout: 60 * 15, 
    maxRetries: 2,
  },
  async function (_, { container }) {
    const logger = container.resolve("logger")
    logger.info("Awaiting driver to claim...")
  }
)
```

This step is async and its only purpose is to wait until it’s marked as successful, which will happen when the driver claims the delivery.

### Create createOrderStep

Create the file `src/workflows/delivery/steps/create-order.ts` with the following content:

```ts title="src/workflows/delivery/steps/create-order.ts" highlights={createOrderStepHighlights1} collapsibleLines="1-9" expandButtonLabel="Show Imports"
import { CreateOrderShippingMethodDTO } from "@medusajs/framework/types"
import {
  Modules,
  ContainerRegistrationKeys,
  MedusaError,
} from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { DELIVERY_MODULE } from "../../../modules/delivery"

export const createOrderStep = createStep(
  "create-order-step",
  async function (deliveryId: string, { container }) {
    const query = container.resolve(ContainerRegistrationKeys.QUERY)

    const { data: [delivery] } = await query.graph({
      entity: "deliveries",
      fields: [
        "id", 
        "cart.*",
        "cart.shipping_address.*",
        "cart.billing_address.*",
        "cart.items.*",
        "cart.shipping_methods.*",
      ],
      filters: {
        id: deliveryId,
      },
    })

    // TODO create order
  },
  async (data, { container }) => {
    // TODO add compensation
  }
)

```

This creates the `createOrderStep`, which so far only retrieves the delivery with its linked cart.

Replace the `TODO` with the following to create the order:

```ts title="src/workflows/delivery/steps/create-order.ts" highlights={createOrderStepHighlights2}
const { cart } = delivery

if (!cart) {
  throw new MedusaError(
    MedusaError.Types.NOT_FOUND,
    `Cart for delivery with id: ${deliveryId} was not found`
  )
}

const orderModuleService = container.resolve(Modules.ORDER)

const order = await orderModuleService.createOrders({
  currency_code: cart.currency_code,
  email: cart.email,
  shipping_address: {
    first_name: cart.shipping_address?.first_name || "",
    last_name: cart.shipping_address?.last_name || "",
    address_1: cart.shipping_address?.address_1 || "",
    address_2: cart.shipping_address?.address_2 || "",
    city: cart.shipping_address?.city || "",
    province: cart.shipping_address?.province || "",
    postal_code: cart.shipping_address?.postal_code || "",
    country_code: cart.shipping_address?.country_code || "",
    phone: cart.shipping_address?.phone || "",
  },
  billing_address: {
    first_name: cart.billing_address?.first_name || "",
    last_name: cart.billing_address?.last_name || "",
    address_1: cart.billing_address?.address_1 || "",
    address_2: cart.billing_address?.address_2 || "",
    city: cart.billing_address?.city || "",
    province: cart.billing_address?.province || "",
    postal_code: cart.billing_address?.postal_code || "",
    country_code: cart.billing_address?.country_code || "",
    phone: cart.billing_address?.phone || "",
  },
  items: cart.items.map((item) => ({
    title: item!.title,
    quantity: item!.quantity,
    variant_id: item!.variant_id || "",
    unit_price: item!.unit_price,
    metadata: item!.metadata || undefined,
    product_id: item!.product_id || "",
  })),
  region_id: cart.region_id || "",
  customer_id: cart.customer_id || "",
  sales_channel_id: cart.sales_channel_id || "",
  shipping_methods:
    cart.shipping_methods?.map((sm): CreateOrderShippingMethodDTO => ({
      shipping_option_id: sm?.shipping_option_id || "",
      data: sm?.data || {},
      name: sm?.name || "",
      amount: sm?.amount || 0,
      order_id: "", // will be set internally
    })) || [],
})

const linkDef = [{
  [DELIVERY_MODULE]: {
    delivery_id: delivery.id as string,
  },
  [Modules.ORDER]: {
    order_id: order.id,
  },
}]

return new StepResponse({ 
  order,
  linkDef,
}, {
  orderId: order.id,
})
```

You create the order using the Order Module’s main service. Then, you create an object holding the links to return. The `createRemoteLinkStep` is used later to create those links.

Then, replace the `TODO` in the compensation function with the following:

```ts title="src/workflows/delivery/steps/create-order.ts"
if (!data) {
  return
}
const orderService = container.resolve(Modules.ORDER)

await orderService.softDeleteOrders([data.orderId])
```

You delete the order in the compensation function.

### create awaitStartPreparationStep

Create the file `src/workflows/delivery/steps/await-start-preparation.ts` with the following content:

```ts title="src/workflows/delivery/steps/await-start-preparation.ts"
import { createStep } from "@medusajs/framework/workflows-sdk"

export const awaitStartPreparationStepId = "await-start-preparation-step"
export const awaitStartPreparationStep = createStep(
  { name: awaitStartPreparationStepId, async: true, timeout: 60 * 15 },
  async function (_, { container }) {
    const logger = container.resolve("logger")
    logger.info("Awaiting start of preparation...")
  }
)
```

This step is async and its only purpose is to wait until it’s marked as successful, which will happen when the restaurant sets the delivery’s status as `restaurant_preparing`.

### create awaitPreparationStep

Create the file `src/workflows/delivery/steps/await-preparation.ts` with the following content:

```ts title="src/workflows/delivery/steps/await-preparation.ts"
import { createStep } from "@medusajs/framework/workflows-sdk"

export const awaitPreparationStepId = "await-preparation-step"
export const awaitPreparationStep = createStep(
  { name: awaitPreparationStepId, async: true, timeout: 60 * 15 },
  async function (_, { container }) {
    const logger = container.resolve("logger")
    logger.info("Awaiting preparation...")
  }
)
```

This step is async and its only purpose is to wait until it’s marked as successful, which will happen when the restaurant sets the delivery’s status as `ready_for_pickup`.

### create createFulfillmentStep

Create the file `src/workflows/delivery/steps/create-fulfillment.ts` with the following content:

```ts title="src/workflows/delivery/steps/create-fulfillment.ts" highlights={createFulfillmentStepHighlights}
import { OrderDTO } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"

export const createFulfillmentStep = createStep(
  "create-fulfillment-step",
  async function (order: OrderDTO, { container }) {
    const fulfillmentModuleService = container.resolve(
      Modules.FULFILLMENT
    )

    const items = order.items?.map((lineItem) => ({
      title: lineItem.title,
      sku: lineItem.variant_sku || "",
      quantity: lineItem.quantity,
      barcode: lineItem.variant_barcode || "",
      line_item_id: lineItem.id,
    }))

    const fulfillment = await fulfillmentModuleService.createFulfillment({
      provider_id: "manual_manual",
      location_id: "1",
      delivery_address: order.shipping_address!,
      items: items || [],
      labels: [],
      order,
    })

    return new StepResponse(fulfillment, fulfillment.id)
  },
  function (id: string, { container }) {
    const fulfillmentModuleService = container.resolve(
      Modules.FULFILLMENT
    )

    return fulfillmentModuleService.cancelFulfillment(id)
  }
)
```

In this step, you retrieve the order’s items as required to create the fulfillment, then create the fulfillment and return it.

In the compensation function, you cancel the fulfillment.

### create awaitPickUpStep

Create the file `src/workflows/delivery/steps/await-pick-up.ts` with the following content:

```ts title="src/workflows/delivery/steps/await-pick-up.ts"
import { createStep } from "@medusajs/framework/workflows-sdk"

export const awaitPickUpStepId = "await-pick-up-step"
export const awaitPickUpStep = createStep(
  { name: awaitPickUpStepId, async: true, timeout: 60 * 15 },
  async function (_, { container }) {
    const logger = container.resolve("logger")
    logger.info("Awaiting pick up by driver...")
  }
)

```

This step is async and its only purpose is to wait until it’s marked as successful, which will happen when the driver sets the delivery’s status as `in_transit`.

### create awaitDeliveryStep

Create the file `src/workflows/delivery/steps/await-delivery.ts` with the following content:

```ts title="src/workflows/delivery/steps/await-delivery.ts"
import { createStep } from "@medusajs/framework/workflows-sdk"

export const awaitDeliveryStepId = "await-delivery-step"
export const awaitDeliveryStep = createStep(
  { name: awaitDeliveryStepId, async: true, timeout: 60 * 15 },
  async function (_, { container }) {
    const logger = container.resolve("logger")
    logger.info("Awaiting delivery by driver...")
  }
)
```

This step is async and its only purpose is to wait until it’s marked as successful, which will happen when the driver sets the delivery’s status as `delivered`.

### create handleDeliveryWorkflow

Finally, create the workflow at `src/workflows/delivery/workflows/handle-delivery.ts`:

```ts title="src/workflows/delivery/workflows/handle-delivery.ts" highlights={handleDeliveryWorkflowHighlights} collapsibleLines="1-15" expandButtonLabel="Show Imports"
import {
  WorkflowResponse,
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
import { setTransactionIdStep } from "../steps/set-transaction-id"
import { notifyRestaurantStep } from "../steps/notify-restaurant"
import { awaitDriverClaimStep } from "../steps/await-driver-claim"
import { createOrderStep } from "../steps/create-order"
import { awaitStartPreparationStep } from "../steps/await-start-preparation"
import { awaitPreparationStep } from "../steps/await-preparation"
import { createFulfillmentStep } from "../steps/create-fulfillment"
import { awaitPickUpStep } from "../steps/await-pick-up"
import { awaitDeliveryStep } from "../steps/await-delivery"

type WorkflowInput = {
  delivery_id: string;
};

const TWO_HOURS = 60 * 60 * 2
export const handleDeliveryWorkflowId = "handle-delivery-workflow"
export const handleDeliveryWorkflow = createWorkflow(
  {
    name: handleDeliveryWorkflowId,
    store: true,
    retentionTime: TWO_HOURS,
  },
  function (input: WorkflowInput) {
    setTransactionIdStep(input.delivery_id)

    notifyRestaurantStep(input.delivery_id)

    awaitDriverClaimStep()

    const { 
      order,
      linkDef,
    } = createOrderStep(input.delivery_id)

    createRemoteLinkStep(linkDef)

    awaitStartPreparationStep()

    awaitPreparationStep()

    createFulfillmentStep(order)

    awaitPickUpStep()

    awaitDeliveryStep()

    return new WorkflowResponse("Delivery completed")
  }
)

```

In the workflow, you execute the steps in the same order mentioned earlier. The workflow has the following options:

- `store` set to `true` to indicate that this workflow’s executions should be stored.
- `retentionTime` which indicates how long the workflow should be stored. It’s set to two hours.

In the next steps, you’ll execute the workflow and see it in action as you add more API routes to handle the delivery.

### Further Reads

- [Long-Running Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md)

***

## Step 13: Create Order Delivery API Route

In this step, you’ll create the API route that executes the workflows created by the previous two steps. This API route is used when a customer places their order.

Create the file `src/api/store/deliveries/route.ts` with the following content:

```ts title="src/api/store/deliveries/route.ts" highlights={createDeliveryRouteHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
import zod from "zod"
import { DELIVERY_MODULE } from "../../../modules/delivery"
import { createDeliveryWorkflow } from "../../../workflows/delivery/workflows/create-delivery"
import { handleDeliveryWorkflow } from "../../../workflows/delivery/workflows/handle-delivery"

const schema = zod.object({
  cart_id: zod.string(),
  restaurant_id: zod.string(),
})

export async function POST(req: MedusaRequest, res: MedusaResponse) {
  const validatedBody = schema.parse(req.body)

  const { result: delivery } = await createDeliveryWorkflow(req.scope).run({
    input: {
      cart_id: validatedBody.cart_id,
      restaurant_id: validatedBody.restaurant_id,
    },
  })

  await handleDeliveryWorkflow(req.scope).run({
    input: {
      delivery_id: delivery.id,
    },
  })

  return res
    .status(200)
    .json({ delivery })
}
```

This adds a `POST` API route at `/deliveries`. It first executes the `createDeliveryWorkflow`, which returns the delivery. Then, it executes the `handleDeliveryWorkflow`.

In the response, it returns the created delivery.

Long-running workflows don’t return the data until it finishes the execution. That’s why you use two workflows instead of one in this API route, as you need to return the created delivery.

### Test it Out

Before using the API route, you must have a cart with at least one product from a restaurant, and with the payment and shipping details set.

### Simplified Steps to Create a Cart

To create a cart:

1. Send a `POST` request to `/store/carts`  to create a cart:

```bash
curl -X POST 'http://localhost:9000/store/carts' \
-H 'Content-Type: application/json' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
--data-raw '{
    "region_id": "reg_01J66SWSSWFZPQV0GWR2W7P1SB",
    "sales_channel_id": "sc_01J66SWSRK95F2P2KTFHMYR7VC",
    "email": "customer@gmail.com",
    "shipping_address": {
        "first_name": "customer",
        "last_name": "test",
        "address_1": "first street",
        "country_code": "dk",
        "city": "Copenhagen",
        "postal_code": "1234"
    },
    "billing_address": {
        "first_name": "customer",
        "last_name": "test",
        "address_1": "first street",
        "country_code": "dk",
        "city": "Copenhagen",
        "postal_code": "1234"
    }
}'
```

Make sure to replace the value of `region_id` and `sales_channel_id` with IDs from your store.

2. Send a `POST` request to `/store/carts/[id]/line-items` to add a product variant to the cart:

```bash
curl -X POST 'http://localhost:9000/store/carts/cart_01J67MS5WPH2CE5R84BENJCGSW/line-items' \
-H 'Content-Type: application/json' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
--data-raw '{
    "variant_id": "variant_01J66T0VC2S0NB4BNDBCZN8P9F",
    "quantity": 1
}'
```

Make sure to replace the cart’s ID in the path parameter, and the variant ID with the ID of a restaurant’s product variant.

3. Send a `GET` request to `/store/shipping-options` to retrieve the shipping options of the cart:

```bash
curl 'http://localhost:9000/store/shipping-options?cart_id=cart_01J67JT10B3RCWKT9NFEFYA2XG' \
-H 'x-publishable-api-key: {your_publishable_api_key}'
```

Make sure to replace the value of the `cart_id` query parameter with the cart’s ID.

4. Copy an ID of a shipping option from the previous request, then send a `POST` request to `/store/carts/[id]/shipping-methods` to set the cart’s shipping method:

```bash
curl -X POST 'http://localhost:9000/store/carts/cart_01J67MS5WPH2CE5R84BENJCGSW/shipping-methods' \
-H 'Content-Type: application/json' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
--data-raw '{
    "option_id": "so_01J66SWSVQG7S1BSJKR7PYWH6C",
    "data": {}
}'
```

Make sure to replace the cart’s ID in the path parameter, and the shipping option’s ID in the request body.

5. Send a `POST` request to `/store/payment-collections` to create a payment collection for your cart:

```bash
curl -X POST 'http://localhost:9000/store/payment-collections' \
-H 'Content-Type: application/json' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
--data-raw '{
    "cart_id": "cart_01J67MS5WPH2CE5R84BENJCGSW"
}'
```

Make sure to replace the cart’s ID in the request body.

6. Send a `POST` request to `/store/payment-collections/[id]/payment-sessions` to initialize a payment session in the payment collection:

```bash
curl -X POST 'http://localhost:9000/store/payment-collections/pay_col_01J67MSK397M3BKD3RDVDMJSRE/payment-sessions' \
-H 'Content-Type: application/json' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
--data-raw '{
    "provider_id": "pp_system_default"
}'
```

Make sure to replace the payment collection’s ID in the path parameter.

After following these steps, you can test out the API route.

Then, send a `POST` request to `/store/deliveries` to create the order delivery:

```bash
curl -X POST 'http://localhost:9000/store/deliveries' \
-H 'Content-Type: application/json' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
--data '{
    "cart_id": "cart_01J67MS5WPH2CE5R84BENJCGSW",
    "restaurant_id": "res_01J66SYN5DSRR0R6QM3A4SYRFZ"
}'
```

Make sure to replace the cart and restaurant’s IDs.

The created delivery is returned in the response. The `handleDeliveryWorkflow` only executes the first two steps, then waits until the `notifyRestaurantStep` is set as successful before continuing.

In the upcoming steps, you’ll add functionalities to update the delivery’s status, which triggers the long-running workflow to continue executing its steps.

***

## Step 14: Accept Delivery API Route

In this step, you’ll create an API route that a restaurant admin uses to accept a delivery. This moves the `handleDeliveryWorkflow` execution from `notifyRestaurantStep` to the next step.

### Add Types

Before implementing the necessary functionalities, add the following types to `src/modules/delivery/types/index.ts`:

```ts title="src/modules/delivery/types/index.ts"
// other imports...
import { InferTypeOf } from "@medusajs/framework/types"
import { Delivery } from "../models/delivery"

// ...

export type Delivery = InferTypeOf<typeof Delivery>

export type UpdateDelivery = Partial<Omit<Delivery, "driver">> & {
  id: string;
  driver_id?: string
}
```

These types are useful in the upcoming implementation steps.

Since the `Delivery` data model is a variable, use `InferTypeOf` to infer its type.

### Create Workflow

As the API route should update the delivery’s status, you’ll create a new workflow to implement that functionality.

The workflow has the following steps:

1. `updateDeliveryStep`: A step that updates the delivery’s data, such as updating its status.
2. `setStepSuccessStep`: A step that changes the status of a step in the delivery’s `handleDeliveryWorkflow` execution to successful. This is useful to move to the next step in the long-running workflow. This step is only used if the necessary input is provided.
3. `setStepFailedStep`: A step that changes the status of a step in the delivery’s `handleDeliveryWorkflow` execution to failed. This step is only used if the necessary input is provided.

So, start by creating the first step at `src/workflows/delivery/steps/update-delivery.ts`:

```ts title="src/workflows/delivery/steps/update-delivery.ts" highlights={updateDeliveryStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { DELIVERY_MODULE } from "../../../modules/delivery"
import { UpdateDelivery } from "../../../modules/delivery/types"
import DeliveryModuleService from "../../../modules/delivery/service"

type UpdateDeliveryStepInput = {
  data: UpdateDelivery;
};

export const updateDeliveryStep = createStep(
  "update-delivery-step",
  async function ({ data }: UpdateDeliveryStepInput, { container }) {
    const deliveryModuleService: DeliveryModuleService = 
      container.resolve(DELIVERY_MODULE)

    const prevDeliveryData = await deliveryModuleService.retrieveDelivery(data.id)

    const delivery = await deliveryModuleService
      .updateDeliveries([data])
      .then((res) => res[0])

    return new StepResponse(delivery, {
      prevDeliveryData,
    })
  },
  async (data, { container }) => {
    if (!data) {
      return
    }
    const deliverModuleService: DeliveryModuleService = 
      container.resolve(DELIVERY_MODULE)

    const { 
      driver, 
      ...prevDeliveryDataWithoutDriver
    } = data.prevDeliveryData

    await deliverModuleService.updateDeliveries(prevDeliveryDataWithoutDriver)
  }
)
```

This step updates a delivery using the provided data. In the compensation function, it reverts the data to its previous state.

Then, create the second step in `src/workflows/delivery/steps/set-step-success.ts`:

```ts title="src/workflows/delivery/steps/set-step-success.ts" highlights={setStepSuccessStepHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports"
import {
  Modules,
  TransactionHandlerType,
} from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { Delivery } from "../../../modules/delivery/types"
import { handleDeliveryWorkflowId } from "../workflows/handle-delivery"

type SetStepSuccessStepInput = {
  stepId: string;
  updatedDelivery: Delivery;
};

export const setStepSuccessStep = createStep(
  "set-step-success-step",
  async function (
    { stepId, updatedDelivery }: SetStepSuccessStepInput,
    { container }
  ) {
    const engineService = container.resolve(
      Modules.WORKFLOW_ENGINE
    )

    await engineService.setStepSuccess({
      idempotencyKey: {
        action: TransactionHandlerType.INVOKE,
        transactionId: updatedDelivery.transaction_id || "",
        stepId,
        workflowId: handleDeliveryWorkflowId,
      },
      stepResponse: new StepResponse(updatedDelivery, updatedDelivery.id),
    })
  }
)

```

This step receives as an input the step’s ID and the associated delivery.

In the step, you resolve the Workflow Engine Module’s service. You then use its `setStepSuccess` method to change the step’s status to success. It accepts details related to the workflow execution’s transaction ID, which is stored in the delivery record, and the step’s response, which is the updated delivery.

Finally, create the last step in `src/workflows/delivery/steps/set-step-failed.ts`:

```ts title="src/workflows/delivery/steps/set-step-failed.ts" highlights={setStepFailedStepHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports"
import {
  Modules,
  TransactionHandlerType,
} from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { Delivery } from "../../../modules/delivery/types"
import { handleDeliveryWorkflowId } from "../../delivery/workflows/handle-delivery"

type SetStepFailedtepInput = {
  stepId: string;
  updatedDelivery: Delivery;
};

export const setStepFailedStep = createStep(
  "set-step-failed-step",
  async function (
    { stepId, updatedDelivery }: SetStepFailedtepInput,
    { container }
  ) {
    const engineService = container.resolve(
      Modules.WORKFLOW_ENGINE
    )

    await engineService.setStepFailure({
      idempotencyKey: {
        action: TransactionHandlerType.INVOKE,
        transactionId: updatedDelivery.transaction_id || "",
        stepId,
        workflowId: handleDeliveryWorkflowId,
      },
      stepResponse: new StepResponse(updatedDelivery, updatedDelivery.id),
    })
  }
)
```

This step is similar to the last one, except it uses the `setStepFailure` method of the Workflow Engine Module’s service to set the status of the step as failed.

You can now create the workflow. Create the file `src/workflows/delivery/workflows/update-delivery.ts` with the following content:

```ts title="src/workflows/delivery/workflows/update-delivery.ts" highlights={updateDeliveryWorkflowHighlights} collapsibleLines="1-10" expandButtonLabel="Show Imports"
import {
  createWorkflow,
  WorkflowResponse,
  when,
} from "@medusajs/framework/workflows-sdk"
import { setStepSuccessStep } from "../steps/set-step-success"
import { setStepFailedStep } from "../steps/set-step-failed"
import { updateDeliveryStep } from "../steps/update-delivery"
import { UpdateDelivery } from "../../../modules/delivery/types"

export type WorkflowInput = {
  data: UpdateDelivery;
  stepIdToSucceed?: string;
  stepIdToFail?: string;
};

export const updateDeliveryWorkflow = createWorkflow(
  "update-delivery-workflow",
  function (input: WorkflowInput) {
    // Update the delivery with the provided data
    const updatedDelivery = updateDeliveryStep({
      data: input.data,
    })

    // If a stepIdToSucceed is provided, we will set that step as successful
    when(input, ({ stepIdToSucceed }) => stepIdToSucceed !== undefined)
      .then(() => {
        setStepSuccessStep({
          stepId: input.stepIdToSucceed!,
          updatedDelivery,
        })
      })

    // If a stepIdToFail is provided, we will set that step as failed
    when(input, ({ stepIdToFail }) => stepIdToFail !== undefined)
      .then(() => {
        setStepFailedStep({
          stepId: input.stepIdToFail!,
          updatedDelivery,
        })
      })

    // Return the updated delivery
    return new WorkflowResponse(updatedDelivery)
  }
)
```

In this workflow, you:

1. Use the `updateDeliveryStep` to update the workflow with the provided data.
2. If `stepIdToSucceed` is provided in the input, you use the `setStepSuccessStep` to set the status of the step to successful.
3. If `stepIdToFail` is provided in the input, you use the `setStepFailedStep` to set the status of the step to failed.

### Create Accept Route

You’ll now use the workflow in the API route that allows restaurant admins to accept an order delivery.

Create the file `src/api/deliveries/[id]/accept/route.ts` with the following content:

```ts title="src/api/deliveries/[id]/accept/route.ts" highlights={acceptRouteHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
import { DeliveryStatus } from "../../../../modules/delivery/types"
import { notifyRestaurantStepId } from "../../../../workflows/delivery/steps/notify-restaurant"
import { updateDeliveryWorkflow } from "../../../../workflows/delivery/workflows/update-delivery"

const DEFAULT_PROCESSING_TIME = 30 * 60 * 1000 // 30 minutes

export async function POST(req: MedusaRequest, res: MedusaResponse) {
  const { id } = req.params

  const eta = new Date(new Date().getTime() + DEFAULT_PROCESSING_TIME)

  const data = {
    id,
    delivery_status: DeliveryStatus.RESTAURANT_ACCEPTED,
    eta,
  }

  const updatedDeliveryResult = await updateDeliveryWorkflow(req.scope)
    .run({
      input: {
        data,
        stepIdToSucceed: notifyRestaurantStepId,
      },
    })
    .catch((error) => {
      console.log(error)
      return MedusaError.Types.UNEXPECTED_STATE
    })

  if (typeof updatedDeliveryResult === "string") {
    throw new MedusaError(updatedDeliveryResult, "An error occurred")
  }

  return res.status(200).json({ delivery: updatedDeliveryResult.result })
}

```

This creates a `POST` API route at `/deliveries/[id]/accept`.

In this route, you calculate an estimated time of arrival (ETA), which is 30 minutes after the current time. You then update the delivery’s `eta` and `status` properties using the `updateDeliveryWorkflow`.

Along with the delivery’s update details, you set the `stepIdToSucceed`'s value to `notifyRestaurantStepId`. This indicates that the `notifyRestaurantStep` should be marked as successful, and the `handleDeliveryWorkflow` workflow execution should move to the next step.

The API route returns the updated delivery.

### Add Middlewares

The above API route should only be accessed by the admin of the restaurant associated with the delivery. So, you must add a middleware that applies an authentication guard on the route.

Start by creating the file `src/api/utils/is-delivery-restaurant.ts` with the following content:

```ts title="src/api/utils/is-delivery-restaurant.ts" highlights={isDeliveryRestaurantHighlights} collapsibleLines="1-11" expandButtonLabel="Show Imports"
import { 
  AuthenticatedMedusaRequest, 
  MedusaNextFunction, 
  MedusaResponse,
} from "@medusajs/framework/http"
import {
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"
import { RESTAURANT_MODULE } from "../../modules/restaurant"
import RestaurantModuleService from "../../modules/restaurant/service"

export const isDeliveryRestaurant = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse,
  next: MedusaNextFunction
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
  const restaurantModuleService: RestaurantModuleService = req.scope.resolve(
    RESTAURANT_MODULE
  )

  const restaurantAdmin = await restaurantModuleService.retrieveRestaurantAdmin(
    req.auth_context.actor_id,
    {
      relations: ["restaurant"],
    }
  )

  const { data: [delivery] } = await query.graph({
    entity: "delivery",
    fields: [
      "restaurant.*",
    ],
    filters: {
      id: req.params.id,
    },
  })

  if (delivery.restaurant?.id !== restaurantAdmin.restaurant.id) {
    return res.status(403).json({
      message: "unauthorized",
    })
  }

  next()
}
```

You define a middleware function that retrieves the currently logged-in restaurant admin and their associated restaurant, and the delivery (whose ID is a path parameter) and its linked restaurant.

The middleware returns an unauthorized response if the delivery’s restaurant isn’t the same as the admin’s restaurant.

Then, create the file `src/api/deliveries/[id]/middlewares.ts` with the following content:

```ts title="src/api/deliveries/[id]/middlewares.ts"
import { 
  authenticate, 
  defineMiddlewares, 
} from "@medusajs/framework/http"
import { isDeliveryRestaurant } from "../../utils/is-delivery-restaurant"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/deliveries/:id/accept",
      middlewares: [
        authenticate("restaurant", "bearer"),
        isDeliveryRestaurant,
      ],
    },
  ],
})
```

This applies two middlewares on the `/deliveries/[id]/accept` API route:

1. The `authenticate` middleware to ensure that only restaurant admins can access this API route.
2. The `isDeliveryRestaurant` middleware that you created above to ensure that only admins of the restaurant associated with the delivery can access the route.

Finally, import and use these middlewares in the main `src/api/middlewares.ts` file:

```ts title="src/api/middlewares.ts"
// other imports...
import deliveriesMiddlewares from "./deliveries/[id]/middlewares"

export default defineMiddlewares({
  routes: [
    // ...
	  ...deliveriesMiddlewares.routes!,
  ],
})
```

### Test it Out

To test the API route out, send a `POST` request to `/deliveries/[id]/accept` as an authenticated restaurant admin:

```bash
curl -X POST 'http://localhost:9000/deliveries/01J67MSXQE59KRBA3C7CJSQM0A/accept' \
-H 'Authorization: Bearer {token}'
```

Make sure to replace the delivery ID in the path and pass the restaurant admin’s authenticated token in the header.

This request returns the updated delivery. If you also check the Medusa application’s logs, you’ll find the following message:

```
Awaiting driver to claim...
```

Meaning that the `handleDeliveryWorkflow`'s execution has moved to the `awaitDriverClaimStep`.

***

## Step 15: Claim Delivery API Route

In this step, you’ll add the API route that allows a driver to claim a delivery.

### Create Workflow

You’ll implement the functionality of claiming a delivery in a workflow that has two steps:

1. `updateDeliveryStep` that updates the delivery’s status to `pickup_claimed` and sets the driver of the delivery.
2. `setStepSuccessStep` that sets the status of the `awaitDriverClaimStep` to successful, moving the `handleDeliveryWorkflow`'s execution to the next step.

So, create the workflow in the file `src/workflows/delivery/workflows/claim-delivery.ts` with the following content:

```ts title="src/workflows/delivery/workflows/claim-delivery.ts" highlights={claimDeliveryWorkflowHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
import {
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { DeliveryStatus } from "../../../modules/delivery/types"
import { awaitDriverClaimStepId } from "../steps/await-driver-claim"
import { setStepSuccessStep } from "../steps/set-step-success"
import { updateDeliveryStep } from "../steps/update-delivery"

export type ClaimWorkflowInput = {
  driver_id: string;
  delivery_id: string;
};

export const claimDeliveryWorkflow = createWorkflow(
  "claim-delivery-workflow",
  function (input: ClaimWorkflowInput) {
    // Update the delivery with the provided data
    const claimedDelivery = updateDeliveryStep({
      data: {
        id: input.delivery_id,
        driver_id: input.driver_id,
        delivery_status: DeliveryStatus.PICKUP_CLAIMED,
      },
    })

    // Set the step success for the find driver step
    setStepSuccessStep({
      stepId: awaitDriverClaimStepId,
      updatedDelivery: claimedDelivery,
    })

    // Return the updated delivery
    return new WorkflowResponse(claimedDelivery)
  }
)
```

In the workflow, you execute the steps as mentioned above and return the updated delivery.

### Create Claim Route

To create the API route to claim the delivery, create the file `src/api/deliveries/[id]/claim/route.ts` with the following content:

```ts title="src/api/deliveries/[id]/claim/route.ts" collapsibleLines="1-9" expandButtonLabel="Show Imports"
import { 
  AuthenticatedMedusaRequest, 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { 
  claimDeliveryWorkflow,
} from "../../../../workflows/delivery/workflows/claim-delivery"

export async function POST(req: AuthenticatedMedusaRequest, res: MedusaResponse) {
  const deliveryId = req.params.id

  const claimedDelivery = await claimDeliveryWorkflow(req.scope).run({
    input: {
      driver_id: req.auth_context.actor_id,
      delivery_id: deliveryId,
    },
  })

  return res.status(200).json({ delivery: claimedDelivery })
}
```

The creates a `POST` API route at `/deliveries/[id]/claim`. In the route, you execute the workflow and return the updated delivery.

### Add Middleware

The above API route should only be accessed by drivers. So, add the following middleware to `src/api/deliveries/[id]/middlewares.ts`:

```ts title="src/api/deliveries/[id]/middlewares.ts"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/deliveries/:id/claim",
      middlewares: [
        authenticate("driver", "bearer"),
      ],
    },
  ],
})
```

This middleware ensures only drivers can access the API route.

### Test it Out

To test it out, first, get the authentication token of a registered driver user:

```bash
curl --location 'http://localhost:9000/auth/driver/emailpass' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "driver@gmail.com",
    "password": "supersecret"
}'
```

Then, send a `POST` request to `/deliveries/[id]/claim`:

```bash
curl -X POST 'http://localhost:9000/deliveries/01J67MSXQE59KRBA3C7CJSQM0A/claim' \
-H 'Authorization: Bearer {token}'
```

Make sure to replace the delivery’s ID in the path parameter and set the driver’s authentication token in the header.

The request returns the updated delivery. If you check the Medusa application’s logs, you’ll find the following message:

```
Awaiting start of preparation...
```

This indicates that the `handleDeliveryWorkflow`'s execution continued past the `awaitDriverClaimStep` until it reached the next async step, which is `awaitStartPreparationStep`.

***

## Step 16: Prepare API Route

In this step, you’ll add the API route that restaurants use to indicate they’re preparing the order.

Create the file `src/api/deliveries/[id]/prepare/route.ts` with the following content:

```ts title="src/api/deliveries/[id]/prepare/route.ts" highlights={prepareRouteHighlights} collapsibleLines="1-10" expandButtonLabel="Show Imports"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
import { DeliveryStatus } from "../../../../modules/delivery/types"
import { 
  updateDeliveryWorkflow,
} from "../../../../workflows/delivery/workflows/update-delivery"
import { 
  awaitStartPreparationStepId,
} from "../../../../workflows/delivery/steps/await-start-preparation"

export async function POST(req: MedusaRequest, res: MedusaResponse) {
  const { id } = req.params

  const data = {
    id,
    delivery_status: DeliveryStatus.RESTAURANT_PREPARING,
  }

  const updatedDelivery = await updateDeliveryWorkflow(req.scope)
    .run({
      input: {
        data,
        stepIdToSucceed: awaitStartPreparationStepId,
      },
    })
    .catch((error) => {
      return MedusaError.Types.UNEXPECTED_STATE
    })

  return res.status(200).json({ delivery: updatedDelivery })
}
```

This creates a `POST` API route at `/deliveries/[id]/prepare`. In this API route, you use the  `updateDeliveryWorkflow` to update the delivery’s status to `restaurant_preparing`, and set the status of the `awaitStartPreparationStep` to successful, moving the `handleDeliveryWorkflow`'s execution to the next step.

### Add Middleware

Since this API route should only be accessed by the admin of a restaurant associated with the delivery, add the following middleware to `src/api/deliveries/[id]/middlewares.ts`:

```ts title="src/api/deliveries/[id]/middlewares.ts"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/deliveries/:id/prepare",
      middlewares: [
        authenticate("restaurant", "bearer"),
        isDeliveryRestaurant,
      ],
    },
  ],
})
```

### Test it Out

Send a `POST` request to `/deliveries/[id]/prepare` as an authenticated restaurant admin:

```bash
curl -X POST 'http://localhost:9000/deliveries/01J67MSXQE59KRBA3C7CJSQM0A/prepare' \
-H 'Authorization: Bearer {token}'
```

Make sure to replace the delivery’s ID in the path parameter and use the restaurant admin’s authentication token in the header.

The request returns the updated delivery. If you check the Medusa application’s logs, you’ll find the following message:

```
Awaiting preparation...
```

This message indicates that the `handleDeliveryWorkflow`'s execution has moved to the next step, which is `awaitPreparationStep`.

***

## Step 17: Ready API Route

In this step, you’ll create the API route that restaurants use to indicate that a delivery is ready for pick up.

Create the file `src/api/deliveries/[id]/ready/route.ts` with the following content:

```ts title="src/api/deliveries/[id]/ready/route.ts" highlights={readyRouteHighlights} collapsibleLines="1-10" expandButtonLabel="Show Imports"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
import { DeliveryStatus } from "../../../../modules/delivery/types"
import { 
  updateDeliveryWorkflow,
} from "../../../../workflows/delivery/workflows/update-delivery"
import { 
  awaitPreparationStepId,
} from "../../../../workflows/delivery/steps/await-preparation"

export async function POST(req: MedusaRequest, res: MedusaResponse) {
  const { id } = req.params

  const data = {
    id,
    delivery_status: DeliveryStatus.READY_FOR_PICKUP,
  }

  const updatedDelivery = await updateDeliveryWorkflow(req.scope)
    .run({
      input: {
        data,
        stepIdToSucceed: awaitPreparationStepId,
      },
    })
    .catch((error) => {
      console.log(error)
      return MedusaError.Types.UNEXPECTED_STATE
    })

  return res.status(200).json({ delivery: updatedDelivery })
}

```

This creates a `POST` API route at `/deliveries/[id]/ready`. In the route, you use the  `updateDeliveryWorkflow` to update the delivery’s status to `ready_for_pickup` and sets the `awaitPreparationStep`'s status to successful, moving the  `handleDeliveryWorkflow`'s execution to the next step.

### Add Middleware

The above API route should only be accessed by restaurant admins. So, add the following middleware to `src/api/deliveries/[id]/middlewares.ts`:

```ts title="src/api/deliveries/[id]/middlewares.ts"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/deliveries/:id/ready",
      middlewares: [
        authenticate("restaurant", "bearer"),
        isDeliveryRestaurant,
      ],
    },
  ],
})
```

### Test it Out

Send a `POST` request to `/deliveries/[id]/ready` as an authenticated restaurant admin:

```bash
curl -X POST 'http://localhost:9000/deliveries/01J67MSXQE59KRBA3C7CJSQM0A/ready' \
-H 'Authorization: Bearer {token}'
```

Make sure to replace the delivery’s ID in the path parameter and use the restaurant admin’s authentication token in the header.

The request returns the updated delivery. If you check the Medusa application’s logs, you’ll find the following message:

```
Awaiting pick up by driver...
```

This message indicates that the `handleDeliveryWorkflow`'s execution has moved to the next step, which is `awaitPickUpStep`.

***

## Step 18: Pick Up Delivery API Route

In this step, you’ll add the API route that the driver uses to indicate they’ve picked up the delivery.

Create the file `src/api/deliveries/[id]/pick-up/route.ts` with the following content:

```ts title="src/api/deliveries/[id]/pick-up/route.ts" highlights={pickUpRouteHighlights} collapsibleLines="1-10" expandButtonLabel="Show Imports"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
import { DeliveryStatus } from "../../../../modules/delivery/types"
import { 
  updateDeliveryWorkflow,
} from "../../../../workflows/delivery/workflows/update-delivery"
import { 
  awaitPickUpStepId,
} from "../../../../workflows/delivery/steps/await-pick-up"

export async function POST(req: MedusaRequest, res: MedusaResponse) {
  const { id } = req.params

  const data = {
    id,
    delivery_status: DeliveryStatus.IN_TRANSIT,
  }

  const updatedDelivery = await updateDeliveryWorkflow(req.scope)
    .run({
      input: {
        data,
        stepIdToSucceed: awaitPickUpStepId,
      },
    })
    .catch((error) => {
      return MedusaError.Types.UNEXPECTED_STATE
    })

  return res.status(200).json({ delivery: updatedDelivery })
}
```

This creates a `POST` API route at `/deliveries/[id]/pick-up`. In this route, you update the delivery’s status to `in_transit` and set the status of the `awaitPickUpStep` to successful, moving the `handleDeliveryWorkflow`'s execution to the next step.

### Add Middleware

The above route should only be accessed by the driver associated with the delivery.

So, create the file `src/api/utils/is-delivery-driver.ts` holding the middleware function that performs the check:

```ts title="src/api/utils/is-delivery-driver.ts" highlights={isDeliveryDriverHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports"
import { 
  AuthenticatedMedusaRequest, 
  MedusaNextFunction, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { DELIVERY_MODULE } from "../../modules/delivery"
import DeliveryModuleService from "../../modules/delivery/service"

export const isDeliveryDriver = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse,
  next: MedusaNextFunction
) => {
  const deliveryModuleService: DeliveryModuleService = req.scope.resolve(
    DELIVERY_MODULE
  )

  const delivery = await deliveryModuleService.retrieveDelivery(
    req.params.id,
    {
      relations: ["driver"],
    }
  )

  if (delivery.driver.id !== req.auth_context.actor_id) {
    return res.status(403).json({
      message: "unauthorized",
    })
  }

  next()
}
```

In this middleware function, you check that the driver is associated with the delivery. If not, you return an unauthorized response.

Then, import and use the middleware function in `src/api/deliveries/[id]/middlewares.ts`:

```ts title="src/api/deliveries/[id]/middlewares.ts"
// other imports...
import { isDeliveryDriver } from "../../utils/is-delivery-driver"

export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/deliveries/:id/pick-up",
      middlewares: [
        authenticate("driver", "bearer"),
        isDeliveryDriver,
      ],
    },
  ],
})
```

This applies the `authenticate` middleware on the `/deliveries/[id]/pick-up` route to ensure only drivers access it, and the `isDeliveryDriver` middleware to ensure only the driver associated with the delivery can access the route.

### Test it Out

Send a `POST` request to `/deliveries/[id]/pick-up` as the authenticated driver that claimed the delivery:

```bash
curl -X POST 'http://localhost:9000/deliveries/01J67MSXQE59KRBA3C7CJSQM0A/pick-up' \
-H 'Authorization: Bearer {token}'
```

Make sure to replace the delivery’s ID in the path parameter and use the driver’s authentication token in the header.

The request returns the updated delivery. If you check the Medusa application’s logs, you’ll find the following message:

```
Awaiting delivery by driver...
```

This message indicates that the `handleDeliveryWorkflow`'s execution has moved to the next step, which is `awaitDeliveryStep`.

***

## Step 19: Complete Delivery API Route

In this step, you’ll create the API route that the driver uses to indicate that they completed the delivery.

Create the file `src/api/deliveries/[id]/complete/route.ts` with the following content:

```ts title="src/api/deliveries/[id]/complete/route.ts" highlights={completeRouteHighlights} collapsibleLines="1-10" expandButtonLabel="Show Imports"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
import { DeliveryStatus } from "../../../../modules/delivery/types"
import { 
  updateDeliveryWorkflow,
} from "../../../../workflows/delivery/workflows/update-delivery"
import { 
  awaitDeliveryStepId,
} from "../../../../workflows/delivery/steps/await-delivery"

export async function POST(req: MedusaRequest, res: MedusaResponse) {
  const { id } = req.params

  const data = {
    id,
    delivery_status: DeliveryStatus.DELIVERED,
    delivered_at: new Date(),
  }

  const updatedDelivery = await updateDeliveryWorkflow(req.scope)
    .run({
      input: {
        data,
        stepIdToSucceed: awaitDeliveryStepId,
      },
    })
    .catch((error) => {
      return MedusaError.Types.UNEXPECTED_STATE
    })

  return res.status(200).json({ delivery: updatedDelivery })
}

```

This adds a `POST` API route at `/deliveries/[id]/complete`. In the API route, you update the delivery’s status to `delivered` and set the status of the `awaitDeliveryStep` to successful, moving the `handleDeliveryWorkflow`'s execution to the next step.

### Add Middleware

The above middleware should only be accessed by the driver associated with the delivery.

So, add the following middlewares to `src/api/deliveries/[id]/middlewares.ts`:

```ts title="src/api/deliveries/[id]/middlewares.ts"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/deliveries/:id/complete",
      middlewares: [
        authenticate("driver", "bearer"),
        isDeliveryDriver,
      ],
    },
  ],
})
```

### Test it Out

Send a `POST` request to `/deliveries/[id]/complete` as the authenticated driver that claimed the delivery:

```bash
curl -X POST 'http://localhost:9000/deliveries/01J67MSXQE59KRBA3C7CJSQM0A/complete' \
-H 'Authorization: Bearer {token}'
```

Make sure to replace the delivery’s ID in the path parameter and use the driver’s authentication token in the header.

The request returns the updated delivery.

As the route sets the status of the `awaitDeliveryStep` to successful in the `handleDeliveryWorkflow`'s execution, this finishes the workflow’s execution.

***

## Step 20: Real-Time Tracking in the Storefront

In this step, you’ll learn how to implement real-time tracking of a delivery in a Next.js-based storefront.

You can use a custom Next.js project, or use Medusa's Next.js Starter storefront. If you haven't installed the Next.js Starter storefront in the first step, refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter#approach-2-install-separately/index.html.md) to learn how to install it.

### Subscribe Route

Before adding the storefront UI, you need an API route that allows a client to stream delivery changes.

So, create the file `src/api/deliveries/[id]/subscribe/route.ts` with the following content:

```ts title="src/api/deliveries/[id]/subscribe/route.ts" collapsibleLines="1-13" expandButtonLabel="Show Imports"
import { 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import {
  Modules,
} from "@medusajs/framework/utils"
import { 
  handleDeliveryWorkflowId,
} from "../../../../../workflows/delivery/workflows/handle-delivery"
import { DELIVERY_MODULE } from "../../../../../modules/delivery"
import DeliveryModuleService from "../../../../modules/delivery/service"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const deliveryModuleService: DeliveryModuleService = req.scope.resolve(
    DELIVERY_MODULE
  )

  const { id } = req.params
  
  const delivery = await deliveryModuleService.retrieveDelivery(id)

  // TODO stream changes
}
```

This creates a `GET` API route at `/deliveries/[id]/subscribe`. Currently, you only retrieve the delivery by its ID.

Next, you’ll stream the changes in the delivery. To do that, replace the `TODO` with the following:

```ts title="src/api/deliveries/subscribe/route.ts"
const headers = {
  "Content-Type": "text/event-stream",
  Connection: "keep-alive",
  "Cache-Control": "no-cache",
}

res.writeHead(200, headers)

// TODO listen to workflow changes

res.write(
  "data: " +
    JSON.stringify({
      message: "Subscribed to workflow",
    transactionId: delivery.transaction_id || undefined,
    }) +
    "\n\n"
)
```

In the above snippet, you set the response to a stream and write an initial message saying that the client is now subscribed to the workflow.

To listen to the workflow changes, replace the new `TODO` with the following:

```ts title="src/api/deliveries/subscribe/route.ts"
const workflowEngine = req.scope.resolve(
  Modules.WORKFLOW_ENGINE
)

const workflowSubHandler = (data: any) => {
  res.write("data: " + JSON.stringify(data) + "\n\n")
}

await workflowEngine.subscribe({
  workflowId: handleDeliveryWorkflowId,
  transactionId: delivery.transaction_id,
  subscriber: workflowSubHandler,
})
```

In this snippet, you resolve the Workflow Engine Module’s main service. Then, you use the `subscribe` method of the service to subscribe to the `handleDeliveryWorkflow` ’s execution. You indicate the execution using the transaction ID stored in the delivery.

### Retrieve Delivery API Route

The storefront UI will also need to retrieve the delivery. So, you’ll create an API route that retrieves the delivery’s details.

Create the file `src/api/deliveries/[id]/route.ts` with the following content:

```ts title="src/api/deliveries/[id]/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { DELIVERY_MODULE } from "../../../../modules/delivery"
import DeliveryModuleService from "../../../../modules/delivery/service"

export async function GET(req: MedusaRequest, res: MedusaResponse) {
  const deliveryModuleService: DeliveryModuleService = 
    req.scope.resolve(DELIVERY_MODULE)

  const delivery = await deliveryModuleService.retrieveDelivery(
    req.params.id
  )

  res.json({
    delivery,
  })
}
```

### Storefront Tracking Page

To implement real-time tracking in a Next.js-based storefront, create the following page:

```tsx title="Storefront Page"
"use client"

import { useRouter } from "next/navigation"
import { useEffect, useState, useTransition } from "react"

type Props = {
  params: { id: string }
}

export default function Delivery({ params: { id } }: Props) {
  const [delivery, setDelivery] = useState<
    Record<string, string> | undefined
  >()
  const router = useRouter()
  const [isPending, startTransition] = useTransition()

  useEffect(() => {
    // TODO retrieve the delivery
  }, [id])

  useEffect(() => {
    // TODO subscribe to the delivery updates
  }, [])

  return (
    <div>
      {isPending && <span>Syncing....</span>}
      {!isPending && delivery && (
        <span>Delivery status: {delivery.delivery_status}</span>
      )}
    </div>
  )
}
```

In this page, you create a `delivery` state variable that you’ll store the delivery in. You also use [React’s useTransition](https://react.dev/reference/react/useTransition) hook to, later, refresh the page when there are changes in the delivery.

To retrieve the delivery from the Medusa application, replace the first `TODO` with the following:

```tsx title="Storefront Page"
// retrieve the delivery
fetch(`http://localhost:9000/store/deliveries/${id}`, {
  credentials: "include",
  headers: {
    "x-publishable-api-key": process.env.NEXT_PUBLIC_PAK,
  },
})
.then((res) => res.json())
.then((data) => {
  setDelivery(data.delivery)
})
.catch((e) => console.error(e))
```

This sends a `GET` request to `/store/deliveries/[id]` to retrieve and set the delivery’s details.

Next, to subscribe to the delivery’s changes in real-time, replace the remaining `TODO` with the following:

```tsx title="Storefront Page"
// subscribe to the delivery updates
const source = new EventSource(
  `http://localhost:9000/deliveries/${id}/subscribe`
)

source.onmessage = (message) => {
  const data = JSON.parse(message.data) as {
    response?: Record<string, unknown>
  }

  if (data.response && "delivery_status" in data.response) {
    setDelivery(data.response as Record<string, string>)
  }

  startTransition(() => {
    router.refresh()
  })
}

return () => {
  source.close()
}
```

You use the [EventSource API](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) to receive the stream from the `/deliveries/[id]/subscribe` API route.

When a new message is set, the new delivery update is extracted from `message.data.response`, if `response` is available and has a `delivery_status` property.

### Test it Out

To test it out, create a delivery order as mentioned in this section. Then, open the page in your storefront.

As you change the delivery’s status using API routes such as `accept` and `claim`, the delivery’s status is updated in the storefront page as well.

***

## Next Steps

The next steps of this example depend on your use case. This section provides some insight into implementing them.

### Admin Development

The Medusa Admin is extendable, allowing you to add widgets to existing pages or create new pages. Learn more about it in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/index.html.md).

### Storefront Development

Medusa provides a Next.js Starter storefront that you can customize to your use case.

You can also create a custom storefront. Check out the [Storefront Development](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/index.html.md) section to learn how to create a storefront.


# Marketplace Recipe: Vendors Example

In this guide, you'll learn how to build a marketplace with Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with support for customizations. While Medusa doesn't provide marketplace functionalities natively, it provides features that you can extend and a Framework to support all your customization needs to build a marketplace.

## Summary

In this guide, you'll customize Medusa to build a marketplace with the following features:

1. Manage multiple vendors, each having vendor admins.
2. Allow vendor admins to manage the vendor’s products and orders.
3. Split orders placed by customers into multiple orders for each vendor.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer.

This guide provides an example of an approach to implement marketplaces. You're free to choose a different approach using the Medusa Framework.

- [Marketplace Example Repository](https://github.com/medusajs/examples/tree/main/marketplace): Find the full code for this recipe in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1720603521/OpenApi/Marketplace_OpenApi_n458oh.yml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. You can also optionally choose to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credential and submit the form. Afterwards, you can login with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Marketplace Module

To add custom tables to the database, which are called data models, you create a [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a re-usable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

In this step, you'll create a Marketplace Module that holds the data models for a vendor and an admin and allows you to manage them.

Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).

### Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/marketplace`.

### Create Data Models

A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

Learn more about data models in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md).

In the Marketplace Module, you'll create two data models:

- `Vendor`: Represents a business that sells its products in the marketplace.
- `VendorAdmin`: Represents an admin of a vendor.

You create a data model in a TypeScript or JavaScript file under the `models` directory of a module. So, to create the `Vendor` data model, create the file `src/modules/marketplace/models/vendor.ts` with the following content:

```ts title="src/modules/marketplace/models/vendor.ts"
import { model } from "@medusajs/framework/utils"
import VendorAdmin from "./vendor-admin"

const Vendor = model.define("vendor", {
  id: model.id().primaryKey(),
  handle: model.text().unique(),
  name: model.text(),
  logo: model.text().nullable(),
  admins: model.hasMany(() => VendorAdmin, {
    mappedBy: "vendor",
  }),
})

export default Vendor
```

You define the data model using DML's `define` method. It accepts two parameters:

1. The first one is the name of the data model's table in the database.
2. The second is an object, which is the data model's schema. The schema's properties are defined using DML methods.

You define the following properties for the `Vendor` data model:

- `id`: A primary key ID for each record.
- `handle`: A unique handle for the vendor. This can be used in URLs on the storefront, such as to show a vendor's details and products.
- `name`: The name of the vendor.
- `logo`: The logo image of a vendor.
- `admins`: The admins of a vendor. It's a relation to the `VendorAdmin` data model which you'll create next.

Learn more about data model [properties](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md) and [relations](https://docs.medusajs.com/docs/learn/fundamentals/data-models/relationships/index.html.md).

Then, to create the `VendorAdmin` data model, create the file `src/modules/marketplace/models/vendor-admin.ts` with the following content:

```ts title="src/modules/marketplace/models/vendor-admin.ts"
import { model } from "@medusajs/framework/utils"
import Vendor from "./vendor"

const VendorAdmin = model.define("vendor_admin", {
  id: model.id().primaryKey(),
  first_name: model.text().nullable(),
  last_name: model.text().nullable(),
  email: model.text().unique(),
  vendor: model.belongsTo(() => Vendor, {
    mappedBy: "admins",
  }),
})

export default VendorAdmin
```

The `VendorAdmin` data model has the following properties:

- `id`: A primary key ID for each record.
- `first_name`: The first name of the admin.
- `last_name`: The last name of the admin.
- `email`: The email of the admin.
- `vendor`: The vendor the admin belongs to. It's a relation to the `Vendor` data model.

### Create Service

You define data-management methods of your data models in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can perform database operations.

Learn more about services in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md).

In this section, you'll create the Marketplace Module's service. Create the file `src/modules/marketplace/service.ts` with the following content:

```ts title="src/modules/marketplace/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import Vendor from "./models/vendor"
import VendorAdmin from "./models/vendor-admin"

class MarketplaceModuleService extends MedusaService({
  Vendor,
  VendorAdmin,
}) { }

export default MarketplaceModuleService
```

The `MarketplaceModuleService` extends `MedusaService` from the Modules SDK which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods.

So, the `MarketplaceModuleService` class now has methods like `createVendors` and `retrieveVendorAdmin`.

Find all methods generated by the `MedusaService` in [this reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md).

You'll use this service in later steps to store and manage vendors and vendor admins.

### Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/marketplace/index.ts` with the following content:

```ts title="src/modules/marketplace/index.ts"
import { Module } from "@medusajs/framework/utils"
import MarketplaceModuleService from "./service"

export const MARKETPLACE_MODULE = "marketplace"

export default Module(MARKETPLACE_MODULE, {
  service: MarketplaceModuleService,
})
```

You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `marketplace`.
2. An object with a required property `service` indicating the module's service.

### Add Module to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/marketplace",
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

### Generate Migrations

Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module.

Learn more about migrations in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md).

Medusa's CLI tool generates the migrations for you. To generate a migration for the Marketplace Module, run the following command in your Medusa application's directory:

```bash
npx medusa db:generate marketplace
```

The `db:generate` command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a `migrations` directory under `src/modules/marketplace` that holds the generated migration.

Then, to reflect the migration and links in the database, run the following command:

```bash
npx medusa db:migrate
```

This will create the tables for the Marketplace Module's data models in the database.

### Further Reads

- [How to Create a Module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md)

***

## Step 3: Define Links to Product and Order Data Models

Modules are [isolated](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md) to ensure they're re-usable and don't have side effects when integrated into the Medusa application. So, to build associations between modules, you define [module links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md). A Module link associates two modules' data models while maintaining module isolation.

Learn more about module links in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md).

Each vendor should have products and orders. So, in this step, you’ll define links between the `Vendor` data model and the `Product` and `Order` data models from the Product and Order modules, respectively.

If your use case requires linking the vendor to other data models, such as `SalesChannel` from the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md), define those links in a similar manner.

To define a link between the `Vendor` and `Product` data models, create the file `src/links/vendor-product.ts` with the following content:

```ts title="src/links/vendor-product.ts"
import { defineLink } from "@medusajs/framework/utils"
import MarketplaceModule from "../modules/marketplace"
import ProductModule from "@medusajs/medusa/product"

export default defineLink(
  MarketplaceModule.linkable.vendor,
  {
    linkable: ProductModule.linkable.product.id,
    isList: true,
  }
)
```

You define a link using `defineLink` from the Modules SDK. It accepts two parameters:

1. The first data model part of the link, which is the Marketplace Module's `vendor` data model. A module has a special `linkable` property that contain link configurations for its data models.
2. The second data model part of the link, which is the Product Module's `product` data model. You also enable `isList`, indicating that a vendor can have many products.

Next, to define a link between the `Vendor` and `Order` data models, create the file `src/links/vendor-order.ts` with the following content:

```ts title="src/links/vendor-order.ts"
import { defineLink } from "@medusajs/framework/utils"
import MarketplaceModule from "../modules/marketplace"
import OrderModule from "@medusajs/medusa/order"

export default defineLink(
  MarketplaceModule.linkable.vendor,
  {
    linkable: OrderModule.linkable.order.id,
    isList: true,
  }
)
```

Similarly, you define an association between the `Vendor` and `Order` data models, where a vendor can have many orders.

In the next steps, you'll see how these link allows you to retrieve and manage a vendor's products and orders.

### Sync Links to Database

Medusa represents the links you define in link tables similar to pivot tables. So, to sync the defined links to the database, run the `db:migrate` command:

```bash
npx medusa db:migrate
```

This command runs any pending migrations and syncs link definitions to the database, creating the necessary tables for your links.

### Further Read

- [How to Define Module Links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md)

***

## Intermission: Understanding Authentication

Before proceeding further, you need to understand some concepts related to authenticating users, especially those of custom actor types.

An [actor type](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-identity-and-actor-types#actor-types/index.html.md) is a type of user that can send an authenticated requests. Medusa has two default actor types: `customer` for customers, and `admin` for admin users.

You can also create custom actor types, allowing you to authenticate your custom users to specific routes. In this recipe, your custom actor type would be the vendor's admin.

When you create a user of the actor type (for example, a vendor admin), you must:

1. Retrieve a registration JWT token. Medusa has a `/auth/{actor_type}/emailpass/register` route to retrieve a registration JWT token for the specified actor type.
2. Create the user. This requires creating the user in the database, and associate an [auth identity](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-identity-and-actor-types#what-is-an-auth-identity/index.html.md) with that user. An auth identity allows this user to later send authenticated requests.
3. Retrieve an authenticated JWT token using Medusa's `/auth/{actor_type}/emailpass` route, which retrieves the token for the specified actor type if the credentials in the request body match a user in the database.

In the next steps, you'll implement the logic to create a vendor and its admin around the above authentication flow. You can also refer to the following documentation pages to learn more about authentication in Medusa:

- [Auth Identities and Actor Types](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-identity-and-actor-types/index.html.md)
- [Authentication Routes](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md)

***

## Step 4: Create Vendor Workflow

To implement and expose a feature that manipulates data, you create a workflow.

A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an endpoint.

In this step, you’ll create the workflow used to create a vendor and its admin. You'll use it in the next step in an API route.

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md)

The workflow’s steps are:

- [createVendorStep](#createvendorstep): Create the vendor.
- [createVendorAdminStep](#createvendoradminstep): Create the vendor admin.
- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md): Associate the vendor admin with its auth identity of actor type \`vendor\`.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the created vendor with its admins.

Medusa provides the last two steps through its `@medusajs/medusa/core-flows` package. So, you only need to implement the first two steps.

### createVendorStep

The first step of the workflow creates the vendor in the database using the Marketplace Module's service.

Create the file `src/workflows/marketplace/create-vendor/steps/create-vendor.ts` with the following content:

```ts title="src/workflows/marketplace/create-vendor/steps/create-vendor.ts" highlights={createVendorHighlights}
import { 
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { MARKETPLACE_MODULE } from "../../../../modules/marketplace"
import MarketplaceModuleService from "../../../../modules/marketplace/service"

type CreateVendorStepInput = {
  name: string
  handle?: string
  logo?: string
}

const createVendorStep = createStep(
  "create-vendor",
  async (vendorData: CreateVendorStepInput, { container }) => {
    const marketplaceModuleService: MarketplaceModuleService = 
      container.resolve(MARKETPLACE_MODULE)

    const vendor = await marketplaceModuleService.createVendors(vendorData)

    return new StepResponse(vendor, vendor.id)
  },
  async (vendorId, { container }) => {
    if (!vendorId) {
      return
    }

    const marketplaceModuleService: MarketplaceModuleService = 
      container.resolve(MARKETPLACE_MODULE)

      marketplaceModuleService.deleteVendors(vendorId)
  }
)

export default createVendorStep
```

You create a step with `createStep` from the Workflows SDK. It accepts three parameters:

1. The step's unique name, which is `create-vendor`.
2. An async function that receives two parameters:
   - An input object with the details of the vendor to create.
   - The [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.
3. An async compensation function. This function is only executed when an error occurs in the workflow. It undoes the changes made by the step.

In the step function, you resolve the Marketplace Module's service from the container. Then, you use the service's generated `createVendors` method to create the vendor.

A step must return an instance of `StepResponse`. It accepts two parameters:

1. The data to return from the step, which is the created vendor in this case.
2. The data to pass as an input to the compensation function.

You pass the vendor's ID to the compensation function. In the compensation function, you delete the vendor if an error occurs in the workflow.

### createVendorAdminStep

The second step of the workflow creates the vendor's admin. So, create the file `src/workflows/marketplace/create-vendor/steps/create-vendor-admin.ts` with the following content:

```ts title="src/workflows/marketplace/create-vendor/steps/create-vendor-admin.ts" highlights={createVendorAdminStepHighlights} collapsibleLines="1-7" expandMoreLabel="Show Imports"
import { 
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import MarketplaceModuleService from "../../../../modules/marketplace/service"
import { MARKETPLACE_MODULE } from "../../../../modules/marketplace"

type CreateVendorAdminStepInput = {
  email: string
  first_name?: string
  last_name?: string
  vendor_id: string
}

const createVendorAdminStep = createStep(
  "create-vendor-admin-step",
  async (
    adminData: CreateVendorAdminStepInput, 
    { container }
  ) => {
    const marketplaceModuleService: MarketplaceModuleService = 
      container.resolve(MARKETPLACE_MODULE)

    const vendorAdmin = await marketplaceModuleService.createVendorAdmins(
      adminData
    )

    return new StepResponse(
      vendorAdmin,
      vendorAdmin.id
    )
  },
  async (vendorAdminId, { container }) => {
    if (!vendorAdminId) {
      return
    }
    
    const marketplaceModuleService: MarketplaceModuleService = 
      container.resolve(MARKETPLACE_MODULE)

    marketplaceModuleService.deleteVendorAdmins(vendorAdminId)
  }
)

export default createVendorAdminStep
```

Similar to the previous step, you create a step that accepts the vendor admin's details as an input, and creates the vendor admin using the Marketplace Module. In the compensation function, you delete the vendor admin if an error occurs.

### Create Workflow

You can now create the workflow that creates a vendor and its admin.

Create the file `src/workflows/marketplace/create-vendor/index.ts` with the following content:

```ts title="src/workflows/marketplace/create-vendor/index.ts" highlights={vendorWorkflowHighlights}
import { 
  createWorkflow,
  WorkflowResponse,
  transform,
} from "@medusajs/framework/workflows-sdk"
import { 
  setAuthAppMetadataStep,
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import createVendorAdminStep from "./steps/create-vendor-admin"
import createVendorStep from "./steps/create-vendor"

export type CreateVendorWorkflowInput = {
  name: string
  handle?: string
  logo?: string
  admin: {
    email: string
    first_name?: string
    last_name?: string
  }
  authIdentityId: string
}

const createVendorWorkflow = createWorkflow(
  "create-vendor",
  function (input: CreateVendorWorkflowInput) {
    const vendor = createVendorStep({
      name: input.name,
      handle: input.handle,
      logo: input.logo,
    })

    const vendorAdminData = transform({
      input,
      vendor,
    }, (data) => {
      return {
        ...data.input.admin,
        vendor_id: data.vendor.id,
      }
    })

    const vendorAdmin = createVendorAdminStep(
      vendorAdminData
    )

    setAuthAppMetadataStep({
      authIdentityId: input.authIdentityId,
      actorType: "vendor",
      value: vendorAdmin.id,
    })
    // @ts-ignore
    const { data: vendorWithAdmin } = useQueryGraphStep({
      entity: "vendor",
      fields: ["id", "name", "handle", "logo", "admins.*"],
      filters: {
        id: vendor.id,
      },
    })

    return new WorkflowResponse({
      vendor: vendorWithAdmin[0],
    })
  }
)

export default createVendorWorkflow
```

You create a workflow with `createWorkflow` from the Workflows SDK. It accepts two parameters:

1. The workflow's unique name, which is `create-vendor`.
2. A function that receives an input object with the details of the vendor and its admin.

In the workflow function, you run the following steps:

1. `createVendorStep` to create the vendor.
2. `createVendorAdminStep` to create the vendor admin.
   - Notice that you use `transform` from the Workflows SDK to prepare the data you pass into the step. Medusa doesn't allow direct manipulation of variables within the workflow's constructor function. Learn more in the [Data Manipulation in Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md).
3. `setAuthAppMetadataStep` to associate the vendor admin with its auth identity of actor type `vendor`. This will allow the vendor admin to send authenticated requests afterwards.
4. `useQueryGraphStep` to retrieve the created vendor with its admins using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). Query allows you to retrieve data across modules.

A workflow must return a `WorkflowResponse` instance. It accepts as a parameter the data to return, which is the vendor in this case.

In the next step, you'll learn how to execute the workflow in an API route.

### Further Read

- [How to Create a Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md)
- [What is an Actor Type](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-identity-and-actor-types/index.html.md)
- [How to Create an Actor Type](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md)
- [What is a Compensation Function](https://docs.medusajs.com/docs/learn/fundamentals/workflows/compensation-function/index.html.md)

***

## Step 5: Create Vendor API Route

Now that you've implemented the logic to create a vendor, you'll expose this functionality in an API route. An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts or custom dashboards.

Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

### Create API Route

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory.

The path of the API route is the file's path relative to `src/api`. So, to create the `/vendors` API route, create the file `src/api/vendors/route.ts` with the following content:

```ts title="src/api/vendors/route.ts" highlights={vendorRouteSchemaHighlights}
import { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
import { z } from "zod"
import createVendorWorkflow, { 
  CreateVendorWorkflowInput,
} from "../../workflows/marketplace/create-vendor"

export const PostVendorCreateSchema = z.object({
  name: z.string(),
  handle: z.string().optional(),
  logo: z.string().optional(),
  admin: z.object({
    email: z.string(),
    first_name: z.string().optional(),
    last_name: z.string().optional(),
  }).strict(),
}).strict()

type RequestBody = z.infer<typeof PostVendorCreateSchema>

```

You start by defining the accepted fields in incoming request bodies using [Zod](https://zod.dev/). You'll later learn how to enforce the schema validation on all incoming requests.

Then, to create the API route, add the following content to the same file:

```ts title="src/api/vendors/route.ts"
export const POST = async (
  req: AuthenticatedMedusaRequest<RequestBody>,
  res: MedusaResponse
) => {
  // If `actor_id` is present, the request carries 
  // authentication for an existing vendor admin
  if (req.auth_context?.actor_id) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "Request already authenticated as a vendor."
    )
  }

  const vendorData = req.validatedBody

  // create vendor admin
  const { result } = await createVendorWorkflow(req.scope)
    .run({
      input: {
        ...vendorData,
        authIdentityId: req.auth_context.auth_identity_id,
      } as CreateVendorWorkflowInput,
    })

  res.json({
    vendor: result.vendor,
  })
}
```

Since you export a `POST` function in this file, you're exposing a `POST` API route at `/vendors`. The route handler function accepts two parameters:

1. A request object with details and context on the request, such as body parameter or authenticated user details.
2. A response object to manipulate and send the response.

In the function, you first check that the user accessing the request isn't already registered (as a vendor admin). Then, you execute the `createVendorWorkflow` from the previous step, passing it the request body.

You also pass the workflow the ID of the auth identity to associate the vendor admin with. This auth identity is set in the request's context because you'll later pass the registration JWT token in the request's header.

Finally, you return the created vendor in the response.

### Apply Authentication and Validation Middlewares

To ensure that incoming request bodies contain the required parameters, and that only vendor admins with a registration token can access this route, you'll add middlewares to the API route.

A middleware is a function executed before the API route when a request is sent to it. Middlewares are useful to restrict access to an API route based on validation or authentication requirements.

Learn more about middlewares in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

You define middlewares in Medusa in the `src/api/middlewares.ts` special file. So, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares, 
  authenticate, 
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { PostVendorCreateSchema } from "./vendors/route"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/vendors",
      method: ["POST"],
      middlewares: [
        authenticate("vendor", ["session", "bearer"], {
          allowUnregistered: true,
        }),
        validateAndTransformBody(PostVendorCreateSchema),
      ],
    },
    {
      matcher: "/vendors/*",
      middlewares: [
        authenticate("vendor", ["session", "bearer"]),
      ],
    },
  ],
})
```

In this file, you export the middlewares definition using `defineMiddlewares` from the Medusa Framework. This function accepts an object having a `routes` property, which is an array of middleware configurations to apply on routes.

You pass in the `routes` array objects having the following properties:

- `matcher`: The route to apply the middleware on.
- `method`: Optional HTTP methods to apply the middleware on for the specified API route.
- `middlewares`: An array of the middlewares to apply.

You first apply two middlewares to the `POST /vendors` API route you just created:

- `authenticate`: Ensure that the user sending the request has a registration JWT token.
- `validateAndTransformBody`: Validate that the incoming request body matches the Zod schema that you created in the API route's file.

You also apply the `authenticate` middleware on all routes starting with `/vendors*` to ensure they can only be accessed by authenticated vendor admin. Note that since you don't enable `allowUnregistered`, the vendor admin must be registered to access these routes.

### Test it Out

To test out the above API route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, you must retrieve a registration JWT token to access the Create Vendor API route. To obtain it, send a `POST` request to the `/auth/vendor/emailpass/register` API route:

```bash
curl -X POST 'http://localhost:9000/auth/vendor/emailpass/register' \
-H 'Content-Type: application/json' \
--data-raw '{
    "email": "vendor@example.com",
    "password": "supersecret"
}'
```

You can replace the email and password with other credentials.

Then, to create a vendor and its admin, send a request to the `/vendors` API route, passing the token retrieved from the previous response in the request header:

Don't include a trailing slash at the end of the URL. Learn more [here](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

```bash
curl -X POST 'http://localhost:9000/vendors' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data-raw '{
    "name": "Acme",
    "handle": "acme",
    "admin": {
        "email": "vendor@example.com",
        "first_name": "Admin",
        "last_name": "Acme"
    }
}'
```

Make sure to replace `{token}` with the registration token you retrieved. If you changed the email previously, make sure to change it here as well.

This will return the created vendor and its admin.

You can now retrieve an authenticated token of the vendor admin. To do that, send a `POST` request to the `/auth/vendor/emailpass` API route:

```bash
curl -X POST 'http://localhost:9000/auth/vendor/emailpass' \
-H 'Content-Type: application/json' \
--data-raw '{
    "email": "vendor@example.com",
    "password": "supersecret"
}'
```

Use this token in the header of later requests that require authentication.

### Further Reads

- [How to Create an API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md)
- [How to Create a Middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md)
- [Learn more about the /auth route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md)

***

## Step 6: Create Product API Route

Now that you support creating vendors, you want to allow these vendors to manage their products.

In this step, you'll create a workflow that creates a product, then use that workflow in a new API route.

### Create Product Workflow

The workflow to create a product has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the default sales channel in the store.
- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md): Create the product.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the admin's vendor ID.
- [createRemoteLinkStep](https://docs.medusajs.com/references/helper-steps/createRemoteLinkStep/index.html.md): Create a link between the vendor and the product.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the created product's details.

The workflow's steps are all provided by Medusa's `@medusajs/medusa/core-flows` package. So, you can create the workflow right away.

Create the file `src/workflows/marketplace/create-vendor-product/index.ts` with the following content:

```ts title="src/workflows/marketplace/create-vendor-product/index.ts"
import { CreateProductWorkflowInputDTO } from "@medusajs/framework/types"
import { 
  createWorkflow, 
  transform, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  createProductsWorkflow, 
  CreateProductsWorkflowInput, 
  createRemoteLinkStep, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { MARKETPLACE_MODULE } from "../../../modules/marketplace"
import { Modules } from "@medusajs/framework/utils"

type WorkflowInput = {
  vendor_admin_id: string
  product: CreateProductWorkflowInputDTO
}

const createVendorProductWorkflow = createWorkflow(
  "create-vendor-product",
  (input: WorkflowInput) => {
    // Retrieve default sales channel to make the product available in.
    // Alternatively, you can link sales channels to vendors and allow vendors
    // to manage sales channels
    const { data: stores } = useQueryGraphStep({
      entity: "store",
      fields: ["default_sales_channel_id"],
    })

    const productData = transform({
      input,
      stores,
    }, (data) => {
      return {
        products: [{
          ...data.input.product,
          sales_channels: [
            {
              id: data.stores[0].default_sales_channel_id,
            },
          ],
        }],
      }
    })

    const createdProducts = createProductsWorkflow.runAsStep({
      input: productData as CreateProductsWorkflowInput,
    })
    
    // TODO link vendor and products
  }
)

export default createVendorProductWorkflow
```

The workflow accepts two parameters:

- `vendor_admin_id`: The ID of the vendor admin creating the product.
- `product`: The details of the product to create.

In the workflow, you first retrieve the default sales channel in the store. This is necessary, as the product can only be purchased in the sales channels it's available in.

Then, you prepare the product's data, combining what's passed in the input and the default sales channel's ID. Finally, you create the product.

Next, you want to create a link between the product and the vendor it's created for. So, replace the `TODO` with the following:

```ts title="src/workflows/marketplace/create-vendor-product/index.ts"
const { data: vendorAdmins } = useQueryGraphStep({
  entity: "vendor_admin",
  fields: ["vendor.id"],
  filters: {
    id: input.vendor_admin_id,
  },
}).config({ name: "retrieve-vendor-admins" })

const linksToCreate = transform({
  input,
  createdProducts,
  vendorAdmins,
}, (data) => {
  return data.createdProducts.map((product) => {
    return {
      [MARKETPLACE_MODULE]: {
        vendor_id: data.vendorAdmins[0].vendor.id,
      },
      [Modules.PRODUCT]: {
        product_id: product.id,
      },
    }
  })
})

createRemoteLinkStep(linksToCreate)

const { data: products } = useQueryGraphStep({
  entity: "product",
  fields: ["*", "variants.*"],
  filters: {
    id: createdProducts[0].id,
  },
}).config({ name: "retrieve-products" })

return new WorkflowResponse({
  product: products[0],
})
```

You retrieve the ID of the admin's vendor. Then, you prepare the data to create a link.

Medusa provides a `createRemoteLinkStep` that allows you to create links between records of different modules. The step accepts as a parameter an array of link objects, where each object has the module name as the key and the ID of the record to link as the value. The modules must be passed in the same order they were passed in to `defineLink`.

Refer to the [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md) documentation to learn more about creating links.

Finally, you retrieve the created product's details using Query and return the product.

### Create API Route

Next, you'll create the API route that uses the above workflow to create a product for a vendor.

Create the file `src/api/vendors/products/route.ts` with the following content:

```ts title="src/api/vendors/products/route.ts"
import { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { 
  HttpTypes,
} from "@medusajs/framework/types"
import createVendorProductWorkflow from "../../../workflows/marketplace/create-vendor-product"

export const POST = async (
  req: AuthenticatedMedusaRequest<HttpTypes.AdminCreateProduct>,
  res: MedusaResponse
) => {
  const { result } = await createVendorProductWorkflow(req.scope)
    .run({
      input: {
        vendor_admin_id: req.auth_context.actor_id,
        product: req.validatedBody,
      },
    })

  res.json({
    product: result.product,
  })
}
```

Since you export a `POST` function, you're exposing a `POST` API route at `/vendors/products`.

In the route handler, you execute the `createVendorProductWorkflow` workflow, passing it the authenticated vendor admin's ID and the request body, which holds the details of the product to create. Finally, you return the product.

### Apply Validation Middleware

Since the above API route requires passing the product's details in the request body, you need to apply a validation middleware on it.

In `src/api/middlewares.ts`, add a new middleware route object:

```ts title="src/api/middlewares.ts"
// other imports...
import { AdminCreateProduct } from "@medusajs/medusa/api/admin/products/validators"

export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/vendors/products",
      method: ["POST"],
      middlewares: [
        validateAndTransformBody(AdminCreateProduct),
      ],
    },
  ],
})
```

Similar to before, you apply the `validateAndTransformBody` middleware on the `POST /vendors/products` API route. You pass to the middleware the `AdminCreateProduct` schema that Medusa uses to validate the request body of the [Create Product Admin API Route](https://docs.medusajs.com/api/admin#products_postproducts).

### Test it Out

To test it out, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, send the following request to `/vendors/products` to create a product for the vendor:

```bash
curl -X POST 'http://localhost:9000/vendors/products' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data '{
    "title": "T-Shirt",
    "status": "published",
    "options": [
        {
            "title": "Color",
            "values": ["Blue"]
        }
    ],
    "variants": [
        {
            "title": "T-Shirt",
            "prices": [
                {
                    "currency_code": "eur",
                    "amount": 10
                }
            ],
            "manage_inventory": false,
            "options": {
                "Color": "Blue"
            }
        }
    ]
}'
```

Make sure to replace `{token}` with the authenticated token of the vendor admin you retrieved earlier.

This will return the created product. In the next step, you'll add API routes to retrieve the vendor's products.

### Further Reads

- [How to use Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md)
- [How to use Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md)

***

## Step 7: Retrieve Products API Route

In this step, you'll add the API route to retrieve a vendor's products.

To create the API route that retrieves the vendor’s products, add the following to `src/api/vendors/products/route.ts`:

```ts title="src/api/vendors/products/route.ts"
// other imports...
import { 
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: [vendorAdmin] } = await query.graph({
    entity: "vendor_admin",
    fields: ["vendor.products.*"],
    filters: {
      id: [
        // ID of the authenticated vendor admin
        req.auth_context.actor_id,
      ],
    },
  })

  res.json({
    products: vendorAdmin.vendor.products,
  })
}
```

You add a `GET` API route at `/vendors/products`. In the route handler, you use Query to retrieve the list of products of the authenticated admin's vendor and returns them in the response. You can retrieve the linked records since Query retrieves data across modules.

### Test it Out

To test out the new API routes, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, send a `GET` request to `/vendors/products` to retrieve the vendor’s products:

```bash
curl 'http://localhost:9000/vendors/products' \
-H 'Authorization: Bearer {token}'
```

Make sure to replace `{token}` with the authenticated token of the vendor admin you retrieved earlier.

### Further Reads

- [How to use Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md)
- [How to use Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md)

***

## Step 8: Create Vendor Order Workflow

In this step, you’ll create a workflow that’s executed when the customer places an order. It has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details.
- [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md): Acquire a lock on the cart to avoid concurrent modifications.
- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md): Create the parent order from the cart.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve existing links between the order and variant to ensure idempotency.
- [getOrderDetailWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrderDetailWorkflow/index.html.md): Retrieve the parent order's details.
- [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md): Release the lock on the cart.

You only need to implement the `groupVendorItemsStep` and `createVendorOrdersStep` steps, as Medusa provides the rest of the steps in its `@medusajs/medusa/core-flows` package.

### groupVendorItemsStep

The third step of the workflow returns an object of items grouped by their vendor.

To create the step, create the file `src/workflows/marketplace/create-vendor-orders/steps/group-vendor-items.ts` with the following content:

```ts title="src/workflows/marketplace/create-vendor-orders/steps/group-vendor-items.ts"
import { 
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { CartLineItemDTO } from "@medusajs/framework/types"
import { ContainerRegistrationKeys, promiseAll } from "@medusajs/framework/utils"

export type GroupVendorItemsStepInput = {
  cart: {
    items?: CartLineItemDTO[]
  }
}

const groupVendorItemsStep = createStep(
  "group-vendor-items",
  async ({ cart }: GroupVendorItemsStepInput, { container }) => {
    const query = container.resolve(ContainerRegistrationKeys.QUERY)

    const vendorsItems: Record<string, CartLineItemDTO[]> = {}

    await promiseAll((cart.items || []).map(async (item) => {
      const { data: [product] } = await query.graph({
        entity: "product",
        fields: ["vendor.*"],
        filters: {
          id: item.product_id || "",
        },
      })

      const vendorId = product.vendor?.id

      if (!vendorId) {
        return
      }
      vendorsItems[vendorId] = [
        ...(vendorsItems[vendorId] || []),
        item,
      ]
    }))

    return new StepResponse({
      vendorsItems,
    })
  }
)

export default groupVendorItemsStep
```

This step receives the cart's details as an input. In the step, you group the items by the vendor associated with the product into an object and returns the object. You use Query to retrieve a product's vendor.

### createVendorOrdersStep

The fourth step of the workflow creates an order for each vendor. The order consists of the items in the parent order that belong to the vendor.

Create the file `src/workflows/marketplace/create-vendor-orders/steps/create-vendor-orders.ts` with the following content:

```ts title="src/workflows/marketplace/create-vendor-orders/steps/create-vendor-orders.ts" highlights={vendorOrder1Highlights} collapsibleLines="1-19" expandMoreLabel="Show Imports"
import { 
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  CartLineItemDTO, 
  OrderDTO,
  LinkDefinition,
  InferTypeOf,
} from "@medusajs/framework/types"
import { Modules, promiseAll } from "@medusajs/framework/utils"
import { 
  cancelOrderWorkflow,
  createOrderWorkflow,
} from "@medusajs/medusa/core-flows"
import MarketplaceModuleService from "../../../../modules/marketplace/service"
import { MARKETPLACE_MODULE } from "../../../../modules/marketplace"
import Vendor from "../../../../modules/marketplace/models/vendor"

export type VendorOrder = (OrderDTO & {
  vendor: InferTypeOf<typeof Vendor>
})

type StepInput = {
  parentOrder: OrderDTO
  vendorsItems: Record<string, CartLineItemDTO[]>
}

function prepareOrderData(
  items: CartLineItemDTO[], 
  parentOrder: OrderDTO
) {
  // TODO format order data
}

const createVendorOrdersStep = createStep(
  "create-vendor-orders",
  async (
    { vendorsItems, parentOrder }: StepInput, 
    { container, context }
  ) => {
    const linkDefs: LinkDefinition[] = []
    const createdOrders: VendorOrder[] = []
    const vendorIds = Object.keys(vendorsItems)
    
    const marketplaceModuleService: MarketplaceModuleService =
      container.resolve(MARKETPLACE_MODULE)

    const vendors = await marketplaceModuleService.listVendors({
      id: vendorIds,
    })

    // TODO create child orders

    return new StepResponse({ 
      orders: createdOrders, 
      linkDefs,
    }, {
      created_orders: createdOrders,
    })
  },
  async (data, { container, context }) => {  
    // TODO add compensation function
  }
)

export default createVendorOrdersStep
```

This creates a step that receives the grouped vendor items and the parent order. For now, it initializes variables and retrieves vendors by their IDs.

The step returns the created orders and the links to be created. It also passes the created orders to the compensation function

Replace the `TODO` in the step with the following:

```ts title="src/workflows/marketplace/create-vendor-orders/steps/create-vendor-orders.ts" highlights={vendorOrder2Highlights}
if (vendorIds.length === 1) {
  linkDefs.push({
    [MARKETPLACE_MODULE]: {
      vendor_id: vendors[0].id,
    },
    [Modules.ORDER]: {
      order_id: parentOrder.id,
    },
  })

  createdOrders.push({
    ...parentOrder,
    vendor: vendors[0],
  })
  
  return new StepResponse({
    orders:  createdOrders,
    linkDefs,
  }, {
    created_orders: [],
  })
}

// TODO create multiple child orders
```

In the above snippet, if there's only one vendor in the group, the parent order is added to the `linkDefs` array and it's returned in the response.

Since the parent order isn't a child order, it's not passed to the compensation function as it should only handle child orders.

Next, replace the new `TODO` with the following snippet:

```ts title="src/workflows/marketplace/create-vendor-orders/steps/create-vendor-orders.ts" highlights={vendorOrder3Highlights}
try {
  await promiseAll(
    vendorIds.map(async (vendorId) => {
      const items = vendorsItems[vendorId]
      const vendor = vendors.find((v) => v.id === vendorId)!

      const { result: childOrder } = await createOrderWorkflow(
        container
      )
      .run({
        input: prepareOrderData(items, parentOrder),
        context,
      }) as unknown as { result: VendorOrder }

      childOrder.vendor = vendor
      createdOrders.push(childOrder)
      
      linkDefs.push({
        [MARKETPLACE_MODULE]: {
          vendor_id: vendor.id,
        },
        [Modules.ORDER]: {
          order_id: childOrder.id,
        },
      })
    })
  )
} catch (e) {
  return StepResponse.permanentFailure(
    `An error occurred while creating vendor orders: ${e}`,
    {
      created_orders: createdOrders,
    }
  )
}
```

In this snippet, you create multiple child orders for each vendor and link the orders to the vendors.

You use `promiseAll` from the Workflows SDK that loops over an array of promises and ensures that all transactions within these promises are rolled back in case an error occurs. You also wrap `promiseAll` in a try-catch block, and in the catch block you invoke and return `StepResponse.permanentFailure` which indicates that the step has failed but still invokes the compensation function that you'll implement in a bit. The first parameter of `permanentFailure` is the error message, and the second is the data to pass to the compensation function.

If an error occurs, the created orders in the `createdOrders` array are canceled using Medusa's `cancelOrderWorkflow` from the `@medusajs/medusa/core-flows` package.

The order's data is formatted using the `prepareOrderData` function. Replace its definition with the following:

```ts title="src/workflows/marketplace/create-vendor-orders/steps/create-vendor-orders.ts"
function prepareOrderData(
  items: CartLineItemDTO[], 
  parentOrder: OrderDTO
) {
  return  {
    items,
    metadata: {
      parent_order_id: parentOrder.id,
    },
    // use info from parent
    region_id: parentOrder.region_id,
    customer_id: parentOrder.customer_id,
    sales_channel_id: parentOrder.sales_channel_id,
    email: parentOrder.email,
    currency_code: parentOrder.currency_code,
    shipping_address_id: parentOrder.shipping_address?.id,
    billing_address_id: parentOrder.billing_address?.id,
    // A better solution would be to have shipping methods for each
    // item/vendor. This requires changes in the storefront to commodate that
    // and passing the item/vendor ID in the `data` property, for example.
    // For simplicity here we just use the same shipping method.
    shipping_methods: parentOrder.shipping_methods?.map((shippingMethod) => ({
      name: shippingMethod.name,
      amount: shippingMethod.amount,
      shipping_option_id: shippingMethod.shipping_option_id,
      data: shippingMethod.data,
      tax_lines: shippingMethod.tax_lines?.map((taxLine) => ({
        code: taxLine.code,
        rate: taxLine.rate,
        provider_id: taxLine.provider_id,
        tax_rate_id: taxLine.tax_rate_id,
        description: taxLine.description,
      })),
      adjustments: shippingMethod.adjustments?.map((adjustment) => ({
        code: adjustment.code,
        amount: adjustment.amount,
        description: adjustment.description,
        promotion_id: adjustment.promotion_id,
        provider_id: adjustment.provider_id,
      })),
    })),
  }
}
```

This formats the order's data using the items and parent order's details.

When creating the child orders, the shipping method of the parent is used as-is for simplicity. A better practice would be to allow the customer to choose different shipping methods for each vendor’s items and then store those details in the `data` property of the shipping method.

Finally, replace the `TODO` in the compensation function with the following:

```ts title="src/workflows/marketplace/create-vendor-orders/steps/create-vendor-orders.ts"
if (!data) {
  return
}
await promiseAll(data.created_orders.map((createdOrder) => {
  return cancelOrderWorkflow(container).run({
    input: {
      order_id: createdOrder.id,
    },
    context,
    container,
  })
}))
```

The compensation function cancels all child orders received from the step. It uses the `cancelOrderWorkflow` that Medusa provides in the `@medusajs/medusa/core-flows` package.

### Create Workflow

Now that you have all the necessary steps, you can create the workflow.

Create the workflow at the file `src/workflows/marketplace/create-vendor-orders/index.ts`:

```ts title="src/workflows/marketplace/create-vendor-orders/index.ts" collapsibleLines="1-17" expandMoreLabel="Show Imports"
import { 
  createWorkflow,
  when,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  useQueryGraphStep,
  createRemoteLinkStep,
  completeCartWorkflow,
  getOrderDetailWorkflow,
  acquireLockStep,
  releaseLockStep,
} from "@medusajs/medusa/core-flows"
import groupVendorItemsStep, { GroupVendorItemsStepInput } from "./steps/group-vendor-items"
import createVendorOrdersStep from "./steps/create-vendor-orders"
import vendorOrderLink from "../../../links/vendor-order"

type WorkflowInput = {
  cart_id: string
}

const createVendorOrdersWorkflow = createWorkflow(
  "create-vendor-order",
  (input: WorkflowInput) => {
    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      fields: ["id", "items.*"],
      filters: { id: input.cart_id },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    acquireLockStep({
      key: input.cart_id,
      timeout: 2,
      ttl: 10,
    })

    const { id: orderId } = completeCartWorkflow.runAsStep({
      input: {
        id: carts[0].id,
      },
    })

    const { data: existingLinks } = useQueryGraphStep({
      entity: vendorOrderLink.entryPoint,
      fields: ["vendor.id"],
      filters: { order_id: orderId },
    }).config({ name: "retrieve-existing-links" })
      
    const order = getOrderDetailWorkflow.runAsStep({
      input: {
        order_id: orderId,
        fields: [
          "region_id",
          "customer_id",
          "sales_channel_id",
          "email",
          "currency_code",
          "shipping_address.*",
          "billing_address.*",
          "shipping_methods.*",
          "shipping_methods.tax_lines.*",
          "shipping_methods.adjustments.*",
        ],
      },
    })

    const vendorOrders = when(
      "create-vendor-order-links",
      { existingLinks },
      (data) => data.existingLinks.length === 0
    ).then(() => {

      const { vendorsItems } = groupVendorItemsStep({
        cart: carts[0],
      } as unknown as GroupVendorItemsStepInput)
  
      const { 
        orders: vendorOrders, 
        linkDefs,
      } = createVendorOrdersStep({
        parentOrder: order,
        vendorsItems,
      })
  
      createRemoteLinkStep(linkDefs)

      return vendorOrders
    })

    releaseLockStep({
      key: input.cart_id,
    })

    return new WorkflowResponse({
      order,
      vendorOrders,
    })
  }
)

export default createVendorOrdersWorkflow
```

The workflow receives the cart's ID as an input. In the workflow, you run the following steps:

1. `useQueryGraphStep` to retrieve the cart's details.
2. `acquireLockStep` to acquire a lock on the cart.
3. `completeCartWorkflow` to complete the cart and create a parent order.
4. `useQueryGraphStep` to retrieve existing links between the order and variant to ensure idempotency.
   - This is essential, as the workflow might be executed multiple times for the same cart. So, if links already exist, you skip creating vendor orders again.
5. `getOrderDetailWorkflow` to retrieve the parent order's details.
6. Perform a condition with `when` to check if there are existing links. If not, you run the following steps:
   - `groupVendorItemsStep` to group the items by their vendor.
   - `createVendorOrdersStep` to create child orders for each vendor.
   - `createRemoteLinkStep` to create the links returned by the previous step.
7. `releaseLockStep` to release the lock on the cart.

You return the parent and vendor orders.

### Create API Route Executing the Workflow

You’ll now create the API route that executes the workflow.

Create the file `src/api/store/carts/[id]/complete-vendor/route.ts` with the following content:

```ts title="src/api/store/carts/[id]/complete-vendor/route.ts"
import { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import createVendorOrdersWorkflow from "../../../../../workflows/marketplace/create-vendor-orders"

export const POST = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const cartId = req.params.id

  const { result } = await createVendorOrdersWorkflow(req.scope)
    .run({
      input: {
        cart_id: cartId,
      },
    })

  res.json({
    type: "order",
    order: result.parent_order,
  })
}
```

Since you expose a `POST` function, you're exposing a `POST` API route at `/store/carts/:id/complete-vendor`. In the route handler, you execute the `createVendorOrdersWorkflow` and return the created order.

### Test it Out

To test this out, it’s recommended to install the [Next.js Starter storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).

Then, you need to customize the storefront to use your complete cart API route rather than Medusa's. In `src/lib/data/cart.ts`, find the following lines in the `src/lib/data/cart.ts`:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
const cartRes = await sdk.store.cart
  .complete(id, {}, headers)
  .then(async (cartRes) => {
    const cartCacheTag = await getCacheTag("carts")
    revalidateTag(cartCacheTag)
    return cartRes
  })
  .catch(medusaError)
```

Replace them with the following:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
const cartRes = await sdk.client.fetch<HttpTypes.StoreCompleteCartResponse>(
  `/store/carts/${id}/complete-vendor`, {
    method: "POST",
    headers,
  })
  .then(async (cartRes) => {
    const cartCacheTag = await getCacheTag("carts")
    revalidateTag(cartCacheTag)
    return cartRes
  })
  .catch(medusaError)
```

Now, the checkout flow uses your custom API route to place the order instead of Medusa's.

Refer to the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) documentation to learn more about using it.

Try going through the checkout flow now, purchasing a product that you created for the vendor earlier. The order should be placed successfully.

In the next step, you'll create an API route to retrieve the vendor's orders, allowing you to confirm that the child order was created for the vendor.

***

## Step 9: Retrieve Vendor Orders API Route

In this step, you’ll create an API route that retrieves a vendor’s orders. Create the file `src/api/vendors/orders/route.ts` with the following content:

```ts title="src/api/vendors/orders/route.ts" highlights={getOrderHighlights}
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { getOrdersListWorkflow } from "@medusajs/medusa/core-flows"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: [vendorAdmin] } = await query.graph({
    entity: "vendor_admin",
    fields: ["vendor.orders.*"],
    filters: {
      id: [req.auth_context.actor_id],
    },
  })

  const { result: orders } = await getOrdersListWorkflow(req.scope)
    .run({
      input: {
        fields: [
          "metadata",
          "total",
          "subtotal",
          "shipping_total",
          "tax_total",
          "items.*",
          "items.tax_lines",
          "items.adjustments",
          "items.variant",
          "items.variant.product",
          "items.detail",
          "shipping_methods",
          "payment_collections",
          "fulfillments",
        ],
        variables: {
          filters: {
            id: vendorAdmin.vendor.orders?.map((order) => order?.id),
          },
        },
      },
    })

  res.json({
    orders,
  })
}
```

You add a `GET` API route at `/vendors/orders`. In the route handler, you first use Query to retrieve the orders of the authenticated admin's vendor. Then, you use Medusa's `getOrdersListWorkflow` to retrieve the list of orders with the specified fields.

### Test it Out

To test it out, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, send a `GET` request to `/vendors/orders` :

```bash
curl 'http://localhost:9000/vendors/orders' \
-H 'Authorization: Bearer {token}'
```

Make sure to replace the `{token}` with the vendor admin’s authentication token.

You’ll receive in the response the orders of the vendor created in the previous step.

***

## Next Steps

The next steps of this example depend on your use case. This section provides some insight into implementing them.

### Use Existing Features

If you want vendors to perform actions that are available for admin users through Medusa's [Admin API routes](https://docs.medusajs.com/api/admin), such as managing their orders, you need to recreate them similar to the create product API route you created earlier.

### Link Other Data Models to Vendors

Similar to linking an order and a product to a vendor, you can link other data models to vendors as well.

For example, you can link sales channels or other settings to vendors.

[Learn more about module links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md).

### Storefront Development

Medusa provides a Next.js Starter storefront, which you can customize to fit your specific use case.

You can also create a custom storefront. Check out the [Storefront Development](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/index.html.md) section to learn how to create a storefront.

### Admin Development

The Medusa Admin is extendable, allowing you to add custom widgets to existing pages or create entirely new pages. For example, you can add a new page showing the list of vendors. Learn more about it in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/index.html.md).

Only super admins can access the Medusa Admin, not vendor admins. So, if you need a dashboard specific to each vendor admin, you will need to build a custom dashboard with the necessary features.


# Marketplace Recipe

This recipe provides the general steps to implement a marketplace in your Medusa application.

## Example Guides

## Overview

A marketplace is an online commerce store that allows different vendors to sell their products within the same commerce system. Customers can purchase products from any of these vendors, and vendors can manage their orders separately.

Medusa's [Framework](https://docs.medusajs.com/docs/learn/fundamentals/framework/index.html.md) for customizations facilitates building a marketplace. You can create a Marketplace Module that implements custom data models, such as vendors or sellers, and link those data models to existing ones such as products and orders. You also expose custom features using API routes, and implement complex flows using workflows.

[How Foraged built a custom marketplace with Medusa](https://medusajs.com/blog/foraged/).

***

## Create Custom Module with Data Models

In a marketplace, a business or a vendor has a user, and they can use that user to authenticate and manage the vendor's data.

You can create a marketplace module that implements data models for vendors, their admins, and any other data models that fit your use case.

- [Create a Module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md): Learn how to create a module.
- [Create Data Models](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md): Create data models in the module.

***

## Link Custom and Existing Data Models

Since a vendor has products, orders, and other models based on your use case, you can define module links between your module's data models and the Commerce Module's data models.

For example, if you defined a vendor data model in a marketplace module, you can define a module link between the vendor and the Product Module's product data model. This builds an association between a vendor and their products, allowing you to query and manage products based on the vendor.

[Define a Module Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md): Learn how to define a module link.

***

## Create Vendor API Routes

Your marketplace will most likely provide custom features for vendors, such as managing their products and orders. You can create API routes that expose these features to the vendors.

When you build these API routes, it's essential that you protect them to only allow authenticated vendors. For example, only a vendor's admin should be able to manage their products and orders.

Medusa supports creating custom actor types that can be authenticated with your custom API routes.

- [Create API Routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md): Learn how to create an API Route in Medusa.
- [Create an Actor Type](https://docs.medusajs.com/commerce-modules/auth/create-actor-type/index.html.md): Learn how to create an actor type and authenticate it.

***

## Split Orders Based on Vendors

If your use case allows a customer's orders to have items from different vendors, you can replicate the [Complete Cart API route](https://docs.medusajs.com/api/store#carts_postcartsidcomplete) to customize the order creation process.

In the API route, you can create a workflow that splits the order into multiple orders, one for each vendor. A workflow is a series of steps that provide features like rollback and retry mechanisms.

- [Replicate API Routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/override/index.html.md): Learn how to replicate an existing API route.
- [Create a Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md): Learn how to create a workflow in Medusa.

***

## Customize Admin Dashboard

Based on your use case, you may need to customize the Medusa Admin to add new widgets or pages.

For example, you can create a page that lists all vendors or a widget that shows a product's vendor information.

The Medusa Admin is an extensible application within your Medusa application. You can customize it by:

- **Widgets**: Adding widgets to existing pages, such as the product page.
- **UI Routes**: Adding new pages to the Medusa Admin, such as a page to manage vendors.
- **Settings Pages**: Adding new pages to the Medusa Admin settings, such as a page to manage marketplace settings.

- [Create Admin Widget](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md): Add widgets into existing admin pages.
- [Create Admin UI Routes](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md): Add new pages to your Medusa Admin.

[Create Admin Setting Page](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes#create-settings-page/index.html.md): Add new page to the Medusa Admin settings.

***

## Build Dashboard for Vendors

For more complex use cases, customizing the Medusa Admin may not be enough to allow vendors to manage their data.

In that case, you can build a custom dashboard for vendors that allows them to manage their data. This dashboard can interact with Medusa's Admin API and the custom API routes you created for vendors to provide a seamless experience.

[Medusa Admin APIs](https://docs.medusajs.com/api/admin): Learn about available APIs for the Medusa Admin.

***

## Customize or Build Storefront

Medusa provides a Next.js Starter Storefront to use with your application. You can customize it for your marketplace use case, such as showing products by vendor.

Alternatively, you can build your own storefront using the Medusa APIs. This headless approach gives you the flexibility to build a custom storefront without limitations on which tech stack you use, or the design of the storefront.

- [Next.js Starter Storefront](https://docs.medusajs.com/nextjs-starter/index.html.md): Learn how to install and customize the Next.js Starter Storefront.
- [Storefront Development](https://docs.medusajs.com/storefront-development/index.html.md): Find useful guides for creating a custom storefront.


# Multi-Region Store Recipe

This recipe provides the general steps to build a multi-region store with Medusa.

## Overview

A multi-regional store allows merchants to sell across different countries. This includes supporting each country's tax rules, currency, available shipping and payment options, and more.

Medusa comes with multi-regional support out of the box. This recipe explains how to benefit from Medusa's features to create a multi-regional store.

***

## Multi-Region Setup

In Medusa, you can create unlimited regions in your store. Each region has configurations managed through the Medusa Admin or the Admin REST APIs.

### Currency

Merchants specify the currency of each region. Multiple regions can have the same currency, but a region has only one currency.

When customers view your products from a region, they see the prices in the region’s currency.

### Tax Regions and Rates

Merchants can define tax regions, which are tax-related settings for a specific country. For each tax region, merchants can set a default tax rate and override it with tax rates for specific conditions, such as product types.

During checkout, Medusa calculates the taxes using the tax region settings of the customer's region and selected country in their shipping address.

- [Using Medusa Admin](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md): Learn how to manage tax regions in the Medusa Admin
- [Using Admin APIs](https://docs.medusajs.com/api/admin#tax-regions_posttaxregions): Manage tax regions using the Admin APIs.

### Payment and Fulfillment Providers

Merchants choose which payment providers are available in each region. For example, one region can use Payment Provider A and B while another only uses Payment Provider B.

Merchants can also choose the fulfillment providers available in each stock location, and provide shipping options using the providers in those locations.

During checkout, customers only see the payment providers configured for the region, and they can only choose shipping options that can be used to fulfill items to their shipping address. This allows merchants to give customers a localized experience that feels familiar and instills trust.

Medusa provides official module providers for payment and fulfillment. You can also create custom module providers.

- [Manage Payment Providers in Medusa Admin](https://docs.medusajs.com/user-guide/settings/regions/index.html.md): Learn how to manage providers in a region.
- [Manage Fulfillment Providers in Medusa Admin](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations#manage-fulfillment-providers/index.html.md): Learn how to manage providers in a location.
- [Integrations](https://docs.medusajs.com/integrations/index.html.md): Check out available integrations, including payment module providers.
- [Create Fulfillment Module Provider](https://docs.medusajs.com/references/fulfillment/provider/index.html.md): Learn how to create a fulfillment module provider.

***

## Prices Per Region and Currency

Merchants set the price of shipping options and product variants per currency and region. This also applies to adding sales or overriding prices for specific conditions.

Using the tax-inclusive feature, merchants can also specify prices including taxes per currency and region. Medusa then calculates the tax amount applied to a line item in the cart based on the region's tax configurations.

- [Setting Variant Prices in Medusa Admin](https://docs.medusajs.com/user-guide/products/variants#edit-product-variant-prices/index.html.md): Learn how to set a variant's prices in Medusa Admin.
- [Display Variant Price in Storefront](https://docs.medusajs.com/storefront-development/products/price/index.html.md): Learn how to display the correct product price in a storefront.

***

## Multi-Warehouse Support

Medusa's [Inventory](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) and [Stock Location](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md) Modules provide multi-warehouse features that allow merchants to manage inventory across different locations. Merchants then control which location an item in an order is fulfilled from, allowing them to keep a correct inventory count across locations and sales channels.

A multi-regional setup lets merchants manage their inventory through Medusa across the different regions they serve. Customers are always shown accurate inventory information based on the location associated with their sales channel.

- [Manage Stock Locations](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations/index.html.md): Learn how to manage stock locations in the Medusa Admin.
- [Manage Inventory](https://docs.medusajs.com/user-guide/inventory/index.html.md): Learn how to manage inventory in the Medusa Admin.

***

## Multi-Lingual Setup

Medusa's [Translation Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/translation/index.html.md) allows merchants to manage translations for product-related resources through Medusa's Store API routes.

This enables merchants to provide a localized experience for customers in different regions by serving translated content in their storefront applications.

Future versions of the Translation Module will expand translation support to additional resources beyond product-related ones.

- [Manage Translations](https://docs.medusajs.com/user-guide/settings/translations/index.html.md): Learn how to manage translations in the Medusa Admin.
- [Localizing Storefronts](https://docs.medusajs.com/storefront-development/localization/index.html.md): Learn how to localize your storefront applications.


# Omnichannel Commerce Recipe

This recipe provides the general steps to build an omnichannel store with Medusa.

## Overview

Merchants selling across different channels need an efficient way to manage their commerce data and operations across these channels. Omnichannel commerce solves this problem by allowing merchants to support multiple channels through a single system while maintaining layers of separation.

Omnichannel commerce also provides customers a seamless shopping experience, regardless of where they’re shopping. Whether they’re buying your products from your online store, a marketplace like Amazon, or through social media, you provide them with a good experience and handle orders consistently.

Medusa’s modular architecture facilitates building omnichannel commerce, as your server and storefront aren’t tightly coupled. You can also use the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md)'s features to expand your business into other avenues like marketplaces and social commerce.

***

## Create Multiple Storefronts with One Server

When creating an omnichannel commerce, you build multiple webshops or mobile apps. All these storefronts should provide your customers with a similar experience, allow them to browse products, and place orders.

Medusa's commerce features are available as REST APIs that storefronts can access to provide these features for customers. This separation also gives you freedom in choosing the tech stack of the storefronts you’re creating.

[Store REST APIs](https://docs.medusajs.com/api/store): Check out available Store REST APIs in Medusa.

***

## Integrate with Marketplaces and Social Commerce

Businesses are no longer bound to sell in their stores. They can reach their customers through their different shopping channels.

One example is marketplaces like Amazon. Customers searching through Amazon to find products are inadvertently searching through many third-party stores connected to Amazon’s marketplace.

You can implement this example in Medusa with the Sales Channel Module and a custom module. Use the Sales Channel Module's features to set different product availability across sales channels.

Then, in the custom module, integrate with Amazon’s seller program. You can then build workflows that sync your products with Amazon’s marketplace.

Another channel that attracts customer sales is social media. You can create a custom module that integrates with social media apps to show your products and sell them to customers.

- [Sales Channels](https://docs.medusajs.com/commerce-modules/sales-channel/index.html.md): Learn about the Sales Channel Module.
- [Create a Module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md): Learn how to create a module.

[Create a Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows#1-create-workflow/index.html.md): Learn how to create a workflow.

***

## Optimize Customer Experience

Implement the customer journey and design for your storefronts that leads to the best customer experience. The Medusa application doesn't impose any restrictions on how the frontend is built.

Medusa’s architecture also makes it easier to integrate any third-party services to provide a better customer experience:

- Create a module that integrates the third-party service.
- Build a workflow that performs actions spanning across the third-party service and your Medusa application.
- Expose the workflow's features in API routes.
- Send requests to these API routes from your storefront.

- [Create a Module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md): Learn how to create a module.
- [Create a Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md): Learn how to create a workflow.

[Create API Route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md): Learn how to create an API route.


# Order Management System (OMS) Recipe

This recipe provides an overview of Medusa's features and how to use it as an Order Management System (OMS).

## Overview

Building or integrating an OMS brings certain challenges: accepting orders from different sales channels, tracking inventory across the sales channels, integrating third-party fulfillment and payment providers with the OMS, and more.

Medusa's Commerce Modules and [Framework](https://docs.medusajs.com/docs/learn/fundamentals/framework/index.html.md) for customizations allows you to integrate it within a larger ecosystem. The Commerce Modules provide features to allow businesses to accept orders from any sales channel, benefit from multi-warehouse inventory features, and integrate third-party services for fulfillment, payment, and more.

[How Siam Makro used Medusa an OMS](https://medusajs.com/blog/makro-pro/).

***

## Source Orders into Medusa

Sales channels in your commerce ecosystem must route their orders into the OMS.

![Routing orders into Medusa OMS](https://res.cloudinary.com/dza7lstvk/image/upload/v1709032160/Medusa%20Book/oms-orders_zf5ta9.jpg)

Medusa's [Store REST APIs](https://docs.medusajs.com/api/store) let you integrate a checkout experience in any storefront. Alternatively, you can use Medusa's [Draft Order APIs](https://docs.medusajs.com/api/admin#draft-orders) to place an order without direct involvement from the customer, such as when placing an order through a POS.

In addition, you can customize the Medusa application to accept orders through a third-party checkout system. This gives you more flexibility over adding orders to Medusa.

For example, you can support importing orders into Medusa through a custom API Route that allows batch-inserting orders. Another example is creating a scheduled job that runs at a specified interval and imports orders from a third-party service.

- [Store REST APIs](https://docs.medusajs.com/api/store#carts): Learn how to use the Store REST APIs to create an order.
- [Create API Route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md): Learn how to create a custom API Route.

[Create Scheduled Jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md): Learn how to create a scheduled job.

***

## Route Orders to Third-party Fulfillment Services

To integrate third-party fulfillment providers with the Medusa application, you can create a fulfillment module provider.

Medusa uses the Fulfillment Module whenever a fulfillment action is performed, such as when a fulfillment is created for items in an order. The methods of the module's main service use the associated fulfillment module provider to handle the desired fulfillment actions.

![Fulfilling orders with Medusa OMS](https://res.cloudinary.com/dza7lstvk/image/upload/v1709032184/Medusa%20Book/oms-fulfillment_qfrpdd.jpg)

In addition, you can create a subscriber that listens to fulfillment-related events, such as the `order.fulfillment_created` event, to perform actions with the third-party fulfillment provider.

- [Create a Fulfillment Module Provider](https://docs.medusajs.com/references/fulfillment/provider/index.html.md): Learn how to create a fulfillment provider in Medusa.
- [Order Events](https://docs.medusajs.com/references/order/events/index.html.md): Learn about the events emitted related to the Order Module

[Create a Subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md): Learn about create a subscriber

***

## Process Payment with Third-Party Providers

To integrate third-party payment providers with the Medusa application, you can create a payment module provider. Customers can pay for their orders using this providers, and admins can process order payments using it.

In addition, you can create a subscriber that listen to payment-related events, such as the `payment.captured` event, to perform actions in the third-party payment provider.

- [Create a Payment Module Provider](https://docs.medusajs.com/references/payment/provider/index.html.md): Learn how to create a payment module provider.
- [Payment Events](https://docs.medusajs.com/references/payment/events/index.html.md): Learn about the events emitted related to the Payment Module

[Create a Subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md): Learn about create a subscriber

***

## Track Inventory Across Sales Channels

Medusa's Inventory, Stock Location, and Sales Channel modules allow merchants to track inventory levels tied to sales channels across stock locations.

When an order is placed, the item's quantity is reserved from the stock location associated with the order's sales channel.

Once the item is fulfilled, the reserved quantity is deducted from the item's inventory quantity.

- [Inventory Module](https://docs.medusajs.com/commerce-modules/inventory/index.html.md): Learn about the Inventory Module's concepts and features.
- [Stock Location Module](https://docs.medusajs.com/commerce-modules/stock-location/index.html.md): Learn about the Stock Location Module's concepts and features.

[Sales Channel Module](https://docs.medusajs.com/commerce-modules/sales-channel/index.html.md): Learn about the Sales Channel Module's concepts and features.

***

## Handle Returns, Exchanges, and Changes

Customers can return or exchaneg items in an order. A merchant can also edit an order to add, update, or delete items.

When changes are made to an order by any of the mentioned actions, the changes are reflected on the order's totals and associated inventory. The integrated fulfillment and payment module providers are used if fulfillment or payment actions are required, such as fulfilling exchanged items.

Medusa also emits events related to these actions, such as `order.return_requested`. So, you can build a workflow that performs actions with the third-party fulfillment and payment providers, then execute it in a subscriber that's triggered whenever the event is emitted.

- [Order Changes](https://docs.medusajs.com/commerce-modules/order/order-change/index.html.md): Learn about how to use order changes.
- [Order Events](https://docs.medusajs.com/references/order/events/index.html.md): Learn about the events emitted related to the Order Module
- [Create a Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md): Learn how to create a workflow.
- [Create a Subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md): Learn about create a subscriber


# Recipes

This section of the documentation provides recipes for common use cases with example implementations.

## Recipes

- [Marketplace](https://docs.medusajs.com/recipes/marketplace/index.html.md)
- [Subscriptions](https://docs.medusajs.com/recipes/subscriptions/index.html.md)
- [Digital Products](https://docs.medusajs.com/recipes/digital-products/index.html.md)
- [Integrate ERP](https://docs.medusajs.com/recipes/erp/index.html.md)
- [B2B](https://docs.medusajs.com/recipes/b2b/index.html.md)
- [Bundled Products](https://docs.medusajs.com/recipes/bundled-products/index.html.md)
- [Commerce Automation](https://docs.medusajs.com/recipes/commerce-automation/index.html.md)
- [Ecommerce](https://docs.medusajs.com/recipes/ecommerce/index.html.md)
- [Multi-Region Store](https://docs.medusajs.com/recipes/multi-region-store/index.html.md)
- [Omnichannel Store](https://docs.medusajs.com/recipes/omnichannel/index.html.md)
- [OMS](https://docs.medusajs.com/recipes/oms/index.html.md)
- [Personalized Products](https://docs.medusajs.com/recipes/personalized-products/index.html.md)
- [POS](https://docs.medusajs.com/recipes/pos/index.html.md)


# Implement Personalized Products in Medusa

In this tutorial, you will learn how to implement personalized products in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. You can benefit from existing features and extend them to implement your business requirements.

Personalized products allow customers to enter custom values for product attributes, such as text or images, before adding them to the cart. This is useful for businesses that offer customizable products, such as furniture, clothing, or gifts.

## Summary

By following this tutorial, you will learn how to:

- Install and set up a Medusa application.
- Store personalized product data.
- Calculate custom pricing based on personalized attributes.
- Validate personalized product data before adding to the cart.
- Add personalized products to the cart.
- Extend the Medusa Admin dashboard to show personalized product data in an order.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Diagram showcasing requests sent from the storefront to the server to get custom price, add product to cart, and place order. Then the admin user views the personalized items in an order.](https://res.cloudinary.com/dza7lstvk/image/upload/v1753167036/Medusa%20Resources/personalized-products_fgxy6s.jpg)

- [Full Code](https://github.com/medusajs/examples/tree/main/personalized-products): Find the full code for this guide in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1753106919/OpenApi/Personalized-Products_ynzrmj.yaml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.

Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create a Personalized Product

In this tutorial, you'll use the example of selling fabric of custom height and width. The customer can enter the height and width of the fabric they want to buy, and the price will be calculated based on these values.

When the customer adds the product variant to the cart, you'll store the personalized data in the line item's `metadata` property. Note that the same product variant added with different `metadata` to the cart is treated as a separate line item.

When the customer places the order, the line items' `metadata` property are copied to the order's line items, allowing you to access the personalized data later.

So, to create a personalized product, you can just create a [regular product using the Medusa Admin](https://docs.medusajs.com/user-guide/products/create/index.html.md).

### Differentiate Personalized Products

If you want to support both personalized and non-personalized products, you can set an `is_personalized` flag in the personalized product's `metadata` property to differentiate them from regular products.

To do that from the Medusa Admin dashboard:

1. Go to Products from the sidebar and click on the product you want to edit.
2. Click on the <InlineIcon Icon={ArrowUpRightOnBox} alt="external link icon to open metadata editor" /> icon in the "Metadata" section.
3. In the side window, enter `is_personalized` as the key and `true` as the value.
4. Once you're done, click on the "Save" button.

![Screenshot of Medusa Admin metadata editor showing a form field with 'is\_personalized' as the key and 'true' as the value](https://res.cloudinary.com/dza7lstvk/image/upload/v1753108787/Medusa%20Resources/CleanShot_2025-07-21_at_14.55.50_2x_m7ynd7.png)

The rest of this tutorial will always check for this `is_personalized` flag to determine whether the product is personalized or not.

***

## Step 3: Get Personalization Price

When the customer enters the fabric's height and width, you'll calculate a custom price based on the entered dimensions and the product variant's price. You'll show that price to the customer and, later, use it when adding the product to the cart.

If your use case doesn't require custom pricing, you can skip this step and just use the product variant's price as is.

In this step, you'll implement the logic to calculate the custom price, then expose that logic to client applications. To do that, you will:

1. Create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) that calculates the price based on the height, width, and product variant's price.
2. Create an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) that executes the workflow and returns the price.

### a. Create a Workflow

A [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) is a series of queries and actions, called steps, that complete a task. A workflow is similar to a function, but it allows you to track its executions' progress, define roll-back logic, and configure other advanced features.

In Medusa, you implement your custom commerce flows in workflows. Then, you execute those workflows from other customizations, such as API routes.

The workflow that calculates the personalized product variant's price will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Get the region's currency.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Get the product variant's original price.
- [getCustomPriceStep](#getCustomPriceStep): Calculate the custom price based on the height, width, and product variant's price.

Medusa provides the `useQueryGraphStep`, so you only need to implement the `getCustomPriceStep` step.

#### getCustomPriceStep

The `getCustomPriceStep` will calculate a variant's custom price based on its original price, and the height and width values entered by the customer.

To create the step, create the file `src/workflows/steps/get-custom-price.ts` with the following content:

```ts title="src/workflows/steps/get-custom-price.ts" highlights={getCustomPriceStepHighlights}
import { ProductVariantDTO } from "@medusajs/framework/types"
import { MedusaError } from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

export type GetCustomPriceStepInput = {
  variant: ProductVariantDTO & {
    calculated_price?: {
      calculated_amount: number
    }
  }
  metadata?: Record<string, unknown>
}

const DIMENSION_PRICE_FACTOR = 0.01

export const getCustomPriceStep = createStep(
  "get-custom-price",
  async ({
    variant, metadata = {},
  }: GetCustomPriceStepInput) => {
    if (!variant.product?.metadata?.is_personalized) {
      return new StepResponse(variant.calculated_price?.calculated_amount || 0)
    }
    if (!metadata.height || !metadata.width) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Custom price requires width and height metadata to be set."
      )
    }
    const height = metadata.height as number
    const width = metadata.width as number

    const originalPrice = variant.calculated_price?.calculated_amount || 0
    const customPrice = originalPrice + (height * width * DIMENSION_PRICE_FACTOR)

    return new StepResponse(customPrice)
  }
)
```

You create a step with the `createStep` function. It accepts two parameters:

1. The step's unique name.
2. An async function that receives two parameters:
   - The step's input, which is an object with the variant's data and the `metadata` object containing the height and width values.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.

In the step function, you first check if the product is personalized by checking the `is_personalized` flag in the product's metadata. If it is not personalized, you return the original price.

Then, you calculate the custom price by multiplying the height and width to a `0.01` factor, which means that for every square unit of height and width, the price increases by `0.01` units. You add the variant's original price to the result.

Finally, a step function must return a `StepResponse` instance that accepts the step's output as a parameter, which is the calculated price.

#### Create the Workflow

Next, you'll create the workflow that calculates a personalized product variant's price.

Create the file `src/workflows/get-custom-price.ts` with the following content:

```ts title="src/workflows/get-custom-price.ts" highlights={getCustomPriceWorkflowHighlights}
import { QueryContext } from "@medusajs/framework/utils"
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { getCustomPriceStep, GetCustomPriceStepInput } from "./steps/get-custom-price"

type WorkflowInput = {
  variant_id: string
  region_id: string
  metadata?: Record<string, unknown>
}

export const getCustomPriceWorkflow = createWorkflow(
  "get-custom-price-workflow",
  (input: WorkflowInput) => {
    const { data: regions } = useQueryGraphStep({
      entity: "region",
      fields: ["currency_code"],
      filters: {
        id: input.region_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })
    const { data: variants } = useQueryGraphStep({
      entity: "variant",
      fields: [
        "*",
        "calculated_price.*",
        "product.*",
      ],
      filters: {
        id: input.variant_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
      context: {
        calculated_price: QueryContext({
          currency_code: regions[0].currency_code,
        }),
      },
    }).config({ name: "get-custom-price-variant" })

    const price = getCustomPriceStep({
      variant: variants[0],
      metadata: input.metadata,
    } as unknown as GetCustomPriceStepInput)

    return new WorkflowResponse(price)
  }
)
```

You create a workflow using the `createWorkflow` function. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function that holds the workflow's implementation. The function accepts an input object holding the variant's ID, the ID of the customer's region, and the `metadata` object containing the height and width values.

In the workflow, you:

1. Retrieve the region's currency code using the `useQueryGraphStep` step. This is necessary to calculate the variant's price in the correct currency.
   - The `useQueryGraphStep` uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve data across modules.
2. Retrieve the product variant with its calculated price using the `useQueryGraphStep`.
3. Calculate the custom price using the `getCustomPriceStep`, passing the variant and metadata as input.

A workflow must return an instance of `WorkflowResponse` that accepts the data to return to the workflow's executor.

### b. Create an API Route

Now that you have the workflow that calculates the custom price, you can create an API route that executes this workflow and returns the price.

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

Refer to the [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) to learn more about them.

Create the file `src/api/store/variants/[id]/price/route.ts` with the following content:

```ts title="src/api/store/variants/[id]/price/route.ts" highlights={getPriceApiRouteHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { getCustomPriceWorkflow } from "../../../../../workflows/get-custom-price"
import { z } from "zod"

export const PostCustomPriceSchema = z.object({
  region_id: z.string(),
  metadata: z.object({
    height: z.number(),
    width: z.number(),
  }),
})

type PostCustomPriceSchemaType = z.infer<typeof PostCustomPriceSchema>

export async function POST(
  req: MedusaRequest<PostCustomPriceSchemaType>,
  res: MedusaResponse
) {
  const { id: variantId } = req.params
  const { 
    region_id,
    metadata,
  } = req.validatedBody

  const { result: price } = await getCustomPriceWorkflow(req.scope).run({
    input: {
      variant_id: variantId,
      region_id,
      metadata,
    },
  })

  res.json({
    price,
  })
}
```

You create the `PostCustomPriceSchema` schema that is used to validate request bodies sent to this API route with [Zod](https://zod.dev/).

Then, you export a `POST` route handler function, which will expose a `POST` API route at `/store/variants/:id/price`.

In the route handler, you execute the `getCustomPriceWorkflow` workflow by invoking it, passing the Medusa container (stored in `req.scope`), then executing its `run` method.

Finally, you return the price in the response.

### c. Add Validation Middleware

To ensure that the API route receives the correct request body parameters, you can apply a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) on the API route that validates incoming requests.

To apply middlewares, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" highlights={validateAndTransformBodyHighlights}
import { defineMiddlewares, validateAndTransformBody } from "@medusajs/framework/http"
import { PostCustomPriceSchema } from "./store/variants/[id]/price/route"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/store/variants/:id/price",
      methods: ["POST"],
      middlewares: [
        validateAndTransformBody(PostCustomPriceSchema),
      ],
    },
  ],
})
```

You apply Medusa's `validateAndTransformBody` middleware to `POST` requests sent to the `/store/variants/:id/price` route.

The middleware function accepts a Zod schema used for validation. This is the schema you created in the API route's file.

Refer to the [Middlewares](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) documentation to learn more.

You'll test out the API route when you customize the storefront in the next step.

***

## Step 4: Show Calculated Price in the Storefront

In this step, you'll customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to show a personalized product's custom price when the customer enters the height and width values.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-personalized`, you can find the storefront by going back to the parent directory and changing to the `medusa-personalized-storefront` directory:

```bash
cd ../medusa-personalized-storefront # change based on your project name
```

### a. Add Server Function

The first step is to add a server function that retrieves the custom price from the API route you created.

In `src/lib/data/products.ts`, add the following function:

```ts title="src/lib/data/products.ts" badgeLabel="Storefront" badgeColor="blue"
export const getCustomVariantPrice = async ({
  variant_id,
  region_id,
  metadata,
}: {
  variant_id: string
  region_id: string
  metadata?: Record<string, any>
}) => {
  const headers = {
    ...(await getAuthHeaders()),
  }

  return sdk.client
    .fetch<{ price: number }>(
      `/store/variants/${variant_id}/price`,
      {
        method: "POST",
        body: {
          region_id,
          metadata,
        },
        headers,
        cache: "no-cache",
      }
    )
    .then(({ price }) => price)
}
```

You create a `getCustomVariantPrice` function that accepts the variant's ID, the region's ID, and the `metadata` object containing the height and width values.

In the function, you use the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) to send a `POST` request to the `/store/variants/:id/price` API route you created in the previous step. The function returns the price from the response.

### b. Customize the Product Price Component

Next, you'll customize the product price component to show the custom price when the product is personalized.

The `ProductPrice` component is located in `src/modules/products/components/product-price/index.tsx`. It shows either the product's cheapest variant price, or the selected variant's price.

Replace the content of the file with the following:

```tsx title="src/modules/products/components/product-price/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={productPriceHighlights}
import { clx } from "@medusajs/ui"
import { HttpTypes } from "@medusajs/types"
import { useEffect, useMemo, useState } from "react"
import { getCustomVariantPrice } from "../../../../lib/data/products"
import { convertToLocale } from "../../../../lib/util/money"

export default function ProductPrice({
  product,
  variant,
  metadata,
  region,
}: {
  product: HttpTypes.StoreProduct
  variant?: HttpTypes.StoreProductVariant
  metadata?: Record<string, any>
  region: HttpTypes.StoreRegion
}) {
  const [price, setPrice] = useState(0)
  
  useEffect(() => {
    if (
      !variant ||
      (product.metadata?.is_personalized && (
        !metadata?.height || !metadata?.width
      ))
    ) {
      return
    }

    getCustomVariantPrice({
      variant_id: variant.id,
      region_id: region.id,
      metadata,
    })
      .then((price) => {
        setPrice(price)
      })
      .catch((error) => {
        console.error("Error fetching custom variant price:", error)
      })
  }, [metadata, variant])

  const displayPrice = useMemo(() => {
    return convertToLocale({
      amount: price,
      currency_code: region.currency_code,
    })
  }, [price])

  return (
    <div className="flex flex-col text-ui-fg-base">
      <span
        className={clx("text-xl-semi")}
      >
        {price > 0 && <span
          data-testid="product-price"
          data-value={displayPrice}
        >
          {displayPrice}
        </span>}
      </span>
    </div>
  )
}
```

You make the following main changes:

1. Add the `metadata` and `region` props to the component, since you need them to retrieve the custom price.
2. Define a `price` state variable to hold the custom price.
3. Use the `useEffect` hook to call the `getCustomVariantPrice` function whenever the `variant`, `metadata`, or `region` changes.
4. Use the `useMemo` hook to convert the price to the locale format using the `convertToLocale` function.
5. Display the custom price in the component.

### c. Add Height and Width Inputs

Next, you'll customize the parent component of the `ProductPrice` component to pass the new props to it, and to show the height and width inputs.

In `src/modules/products/components/product-actions/index.tsx`, add the following import at the top of the file:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import Input from "../../../common/components/input"
```

Next, pass the `region` prop to the `ProductActions` component. The props' type already defines it but it's not included in the destructured props:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["3"]]}
export default function ProductActions({
  // ...
  region,
}: ProductActionsProps) {
  // ...
}
```

Then, add new state variables in the `ProductActions` component for the height and width values:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const [height, setHeight] = useState(0)
const [width, setWidth] = useState(0)
```

After that, find the `ProductPrice` component in the return statement and replace it with the following:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
    <>
      {/* ... */}
      <div className="flex flex-col gap-y-2">
        {!!product.metadata?.is_personalized && (
          <div className="flex flex-col gap-y-3">
            <span className="text-sm">Enter Dimensions</span>
            <div className="flex gap-3">
              <Input
                name="width"
                value={width}
                onChange={(e) => setWidth(Number(e.target.value))}
                label="Width (cm)"
                type="number"
                min={0}
              />
              <Input
                name="height"
                value={height}
                onChange={(e) => setHeight(Number(e.target.value))}
                label="Height (cm)"
                type="number"
                min={0}
              />
            </div>
          </div>
        )}
      </div>

      <ProductPrice 
        product={product} 
        variant={selectedVariant}
        region={region}
        metadata={{ width, height }}
      />
      {/* ... */}
    </>
)
```

You add a new section that shows the height and width inputs when the product is personalized. The inputs update the `height` and `width` state variables.

Then, you pass the new `metadata` and `region` props to the `ProductPrice` component.

Finally, update the add-to-cart button's `disabled` prop to check if the product is personalized and if the height and width values are set:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["11"]]}
return (
  <>
    {/* ... */}
    <Button
      disabled={
        !inStock ||
        !selectedVariant ||
        !!disabled ||
        isAdding ||
        !isValidVariant ||
        (!!product.metadata?.is_personalized && (!width || !height))
      }
      // ...
    >
      {/* ... */}
    </Button>
    {/* ... */}
  </>
)
```

### Test Price Customization

You can now test the price customization in the storefront.

First, start the Medusa application with the following command:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

Then, start the Next.js Starter Storefront with the following command:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

Open the storefront in your browser at `http://localhost:8000`. Go to Menu -> Store and click on the personalized product you created.

You should see the height and width inputs below other product variant options. Once you enter the height and width values, the price will be shown and updated based on the values you entered.

![Screenshot of a product page in the Next.js storefront showing a fabric product with two input fields labeled 'Width (cm)' and 'Height (cm)' below the product options, and a calculated price displayed underneath the dimension inputs](https://res.cloudinary.com/dza7lstvk/image/upload/v1753111095/Medusa%20Resources/CleanShot_2025-07-21_at_18.17.55_2x_hnzfiz.png)

***

## Step 5: Implement Custom Add-to-Cart Logic

In this step, you'll implement custom logic to add personalized products to the cart.

When the customer adds a personalized product to the cart, you need to add the item to the cart with the calculated price.

So, you'll create a workflow that wraps around Medusa's existing add-to-cart logic to add the personalized product to the cart with the custom price. Then, you'll create an API route that executes this workflow.

If you're not using custom pricing, you can skip this step and keep on using Medusa's existing [Add-to-Cart API route](https://docs.medusajs.com/api/store#carts_postcartsidlineitems).

### a. Create the Add-to-Cart Workflow

The custom add-to-cart workflow will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Get the cart's region.
- [getCustomPriceWorkflow](#getCustomPriceWorkflow): Get the custom price for the product variant.
- [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md): Acquire a lock on the cart to prevent concurrent modifications.
- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md): Add the product variant to the cart with the custom price.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Get the cart's updated data.
- [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md): Release the lock on the cart.

You already have all the necessary steps within the workflow, so you create it right away.

Create the file `src/workflows/custom-add-to-cart.ts` with the following content:

```ts title="src/workflows/custom-add-to-cart.ts" highlights={customAddToCartWorkflowHighlights}
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { 
  addToCartWorkflow, 
  acquireLockStep, 
  releaseLockStep, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { getCustomPriceWorkflow } from "./get-custom-price"

type CustomAddToCartWorkflowInput = {
  item: {
    variant_id: string;
    quantity?: number;
    metadata?: Record<string, unknown>;
  }
  cart_id: string;
}

export const customAddToCartWorkflow = createWorkflow(
  "custom-add-to-cart",
  (input: CustomAddToCartWorkflowInput) => {
    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      fields: ["region_id"],
      filters: {
        id: input.cart_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })
    const price = getCustomPriceWorkflow.runAsStep({
      input: {
        variant_id: input.item.variant_id,
        region_id: carts[0].region_id!,
        metadata: input.item.metadata,
      },
    })
    
    acquireLockStep({
      key: input.cart_id,
      timeout: 2,
      ttl: 10,
    })

    const itemData = transform({
      item: input.item,
      price,
    }, (data) => {
      return {
        variant_id: data.item.variant_id,
        quantity: data.item.quantity || 1,
        metadata: data.item.metadata,
        unit_price: data.price,
      }
    })

    addToCartWorkflow.runAsStep({
      input: {
        cart_id: input.cart_id,
        items: [itemData],
      },
    })

    // refetch the updated cart
    const { data: updatedCart } = useQueryGraphStep({
      entity: "cart",
      fields: ["*", "items.*"],
      filters: {
        id: input.cart_id,
      },
    }).config({ name: "refetch-cart" })

    releaseLockStep({
      key: input.cart_id,
    })

    return new WorkflowResponse({
      cart: updatedCart[0],
    })
  }
)
```

The workflow accepts the item to add to the cart, which includes the variant's ID, quantity, and metadata, as well as the cart's ID.

In the workflow, you:

1. Retrieve the cart's region using the `useQueryGraphStep`.
2. Calculate the custom price using the `getCustomPriceWorkflow` that you created earlier.
3. Acquire a lock on the cart using the `acquireLockStep` to prevent concurrent modifications.
4. Prepare the data of the item to add to the cart.
   - You use the `transform` function because direct data manipulation isn't allowed in workflows. Refer to the [Data Manipulation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) guide to learn more.
5. Add the item to the cart using the `addToCartWorkflow`.
6. Refetch the updated cart using the `useQueryGraphStep` to return the cart data in the workflow's response.
7. Release the lock on the cart using the `releaseLockStep`.

### b. Create the Add-to-Cart API Route

Next, you'll create an API route that executes the custom add-to-cart workflow.

Create the file `src/api/store/carts/[id]/line-items-custom/route.ts` with the following content:

```ts title="src/api/store/carts/[id]/line-items-custom/route.ts" highlights={PostAddCustomLineItemSchemaHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { z } from "zod"
import { 
  customAddToCartWorkflow,
} from "../../../../../workflows/custom-add-to-cart"

export const PostAddCustomLineItemSchema = z.object({
  variant_id: z.string(),
  quantity: z.number().optional(),
  metadata: z.record(z.unknown()).optional(),
})

type PostAddCustomLineItemSchemaType = z.infer<
  typeof PostAddCustomLineItemSchema
>;

export async function POST(
  req: MedusaRequest<PostAddCustomLineItemSchemaType>,
  res: MedusaResponse
) {
  const { id: cartId } = req.params
  const { result: cart } = await customAddToCartWorkflow(req.scope).run({
    input: {
      item: {
        variant_id: req.validatedBody.variant_id,
        quantity: req.validatedBody.quantity,
        metadata: req.validatedBody.metadata,
      },
      cart_id: cartId,
    },
  })

  res.json({
    cart,
  })
}
```

You create a `PostAddCustomLineItemSchema` schema to validate the request body sent to this API route.

Then, you export a `POST` route handler function which will expose a `POST` API route at `/store/carts/:id/line-items-custom`.

In the route handler, you execute the `customAddToCartWorkflow` workflow passing it the item's details and cart ID as input.

Finally, you return the updated cart in the response.

### c. Add Validation Middleware

Similar to the previous API route, you'll apply a validation middleware to the API route to ensure that the request body is valid.

In `src/api/middlewares.ts`, add a new middleware for the custom add-to-cart API route:

```ts title="src/api/middlewares.ts"
// other imports...
import { 
  PostAddCustomLineItemSchema,
} from "./store/carts/[id]/line-items-custom/route"

export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/store/carts/:id/line-items-custom",
      methods: ["POST"],
      middlewares: [
        validateAndTransformBody(PostAddCustomLineItemSchema),
      ],
    },
  ],
})
```

This applies the `validateAndTransformBody` middleware to `POST` requests sent to the `/store/carts/:id/line-items-custom` route, validating the request body against the schema you created.

You'll test out this API route when you customize the storefront in the next steps.

***

## Step 6: Validate Personalized Products Added to Cart

In this step, you'll ensure that the personalized products added to the cart include the height and width values in their `metadata` property.

Medusa's `addToCartWorkflow` workflow supports performing custom validation on the items being added to the cart using [workflow hooks](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md). A workflow hook is a point in a workflow where you can inject custom functionality as a step function.

To consume the `validate` hook that runs before an item is added to the cart, create the file `src/workflows/hooks/validate-personalized-product.ts` with the following content:

```ts title="src/workflows/hooks/validate-personalized-product.ts" highlights={validatePersonalizedProductHighlights}
import { MedusaError } from "@medusajs/framework/utils"
import { addToCartWorkflow } from "@medusajs/medusa/core-flows"

addToCartWorkflow.hooks.validate(
  async ({ input }, { container }) => {
    const query = container.resolve("query")
    const { data: variants } = await query.graph({
      entity: "variant",
      fields: ["product.*"],
      filters: {
        id: input.items.map((item) => item.variant_id).filter(Boolean) as string[],
      },
    })
    for (const item of input.items) {
      const variant = variants.find((v) => v.id === item.variant_id)
      if (!variant?.product?.metadata?.is_personalized) {
        continue
      }
      if (
        !item.metadata?.height || !item.metadata.width ||
        isNaN(Number(item.metadata.height)) || isNaN(Number(item.metadata.width))
      ) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          "Please set height and width metadata for each item."
        )
      }
    }
  }
)
```

You consume the hook by calling `addToCartWorkflow.hooks.validate`, passing it a step function.

In the step function, you:

1. Resolve [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) from the Medusa container to retrieve data across modules.
2. Retrieve the product variants being added to the cart.
3. Loop through the items being added to the cart.
4. If an item's product is personalized, you validate that the `metadata` object contains the `height` and `width` values, and that they are valid numbers. Otherwise, you throw an error.

If the hook throws an error, the `addToCartWorkflow` will not proceed with adding the item to the cart, and the error will be returned in the API response.

Refer to the [Workflow Hooks](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md) documentation to learn more.

You can test this out after customizing the storefront in the next section.

***

## Step 7: Use the Custom Add-to-Cart API Route in Storefront

In this step, you'll customize the Next.js Starter Storefront to use the custom add-to-cart API route when a customer adds a product to the cart.

You'll also customize components showing items in the cart and order confirmation to display the personalized product's height and width values.

### a. Customize Add-to-Cart Server Function

The `addToCart` function defined in `src/lib/data/cart.ts` is used to add items to the cart. You'll customize it to use the custom add-to-cart API route.

First, find the `addToCart` function and change its parameters to accept the `metadata` object:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"], ["10"]]}
export async function addToCart({
  variantId,
  quantity,
  countryCode,
  metadata = {},
}: {
  variantId: string
  quantity: number
  countryCode: string
  metadata?: Record<string, any>
}) {
  // ...
}
```

Then, find the following lines in the function:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
await sdk.store.cart
  .createLineItem(
    cart.id,
    {
      variant_id: variantId,
      quantity,
    },
    {},
    headers
  )
```

And replace them with the following:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
await sdk.client.fetch<{
  cart: HttpTypes.StoreCart
}>(`/store/carts/${cart.id}/line-items-custom`, {
  method: "POST",
  body: {
    variant_id: variantId,
    quantity,
    metadata,
  },
  headers,
})
```

You send a request to the API route you created in the previous step, passing the variant ID, quantity, and metadata in the request body.

Next, you'll need to pass the `metadata` object when calling the `addToCart` function.

In `src/modules/products/components/product-actions/index.tsx`, find the `addToCart` function call in the `handleAddToCart` function and pass it the `metadata` object:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["3"], ["4"], ["5"], ["6"]]}
await addToCart({
  // ...
  metadata: {
    width,
    height,
  },
})
```

Now, when the customer adds a personalized product to the cart, the height and width values will be sent to the API route.

### b. Customize Cart Item Component

Next, you'll customize the cart item component to show the height and width values for personalized products.

In `src/modules/cart/components/item/index.tsx`, add the following in the `Item` component's return statement, right after the `LineItemOptions` component:

```tsx title="src/modules/cart/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"], ["6"], ["7"], ["8"]]}
return (
  <Table.Row className="w-full" data-testid="product-row">
    {/* ... */}
    <LineItemOptions variant={item.variant} data-testid="product-variant" />
    <div className="text-sm text-ui-fg-muted">
      {!!item.metadata?.width && <div>Width: {item.metadata.width as number}cm</div>}
      {!!item.metadata?.height && <div>Height: {item.metadata.height as number}cm</div>}
    </div>
    {/* ... */}
  </Table.Row>
)
```

You show the height and width values from the item's `metadata` object if they exist. This will display the dimensions of personalized products in the cart.

### c. Customize Order Confirmation Page

Finally, you'll customize the order item component in the order confirmation page to show the height and width values for personalized products.

In `src/modules/orders/components/order-item/index.tsx`, add the following in the `Item` component's return statement, right after the `LineItemOptions` component:

```tsx title="src/modules/orders/components/order-item/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"], ["6"], ["7"], ["8"]]}
return (
  <Table.Row className="w-full" data-testid="product-row">
    {/* ... */}
    <LineItemOptions variant={item.variant} data-testid="product-variant" />
    <div className="text-sm text-ui-fg-muted">
      {!!item.metadata?.width && <div>Width: {item.metadata.width as number}cm</div>}
      {!!item.metadata?.height && <div>Height: {item.metadata.height as number}cm</div>}
    </div>
    {/* ... */}
  </Table.Row>
)
```

Similarly, you show the height and width values from the item's `metadata` object if they exist. This will display the dimensions of personalized products in the order confirmation page.

### Test the Custom Add-to-Cart Logic

To test out the custom add-to-cart logic, ensure that both the Medusa application and the Next.js Starter Storefront are running.

Then, add a personalized product to the cart after choosing any necessary product options and entering the height and width values. You should see the product added to the cart with the correct price based on the dimensions you entered.

If you open the cart page by clicking on "Cart" at the top right, you can see the personalized product's height and width values displayed after the product variant options.

![Screenshot of the shopping cart page showing a personalized fabric product with 'Width: 100cm' and 'Height: 80cm' displayed below the product variant information, demonstrating how custom dimensions are preserved in the cart](https://res.cloudinary.com/dza7lstvk/image/upload/v1753112339/Medusa%20Resources/CleanShot_2025-07-21_at_18.38.30_2x_ersc2m.png)

#### Place Order with Personalized Product

You can also proceed to the checkout page and complete the order. The order confirmation page will show the personalized product with its height and width values.

***

## Step 8: Show an Order's Personalized Items in Medusa Admin

In this step, you'll customize the Medusa Admin to show the personalized item's height and width values in an order's details page.

The Medusa Admin dashboard is extensible, allowing you to either inject custom components into existing pages, or create new pages.

In this case, you'll inject a custom component, called a [widget](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md), into the order details page.

Widgets are created in a `.tsx` file under the `src/admin/widgets` directory. So, create the file `src/admin/widgets/order-personalized.tsx` with the following content:

```tsx title="src/admin/widgets/order-personalized.tsx" highlights={personalizedOrderItemsWidgetHighlights}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading, Text } from "@medusajs/ui"
import { AdminOrder, DetailWidgetProps } from "@medusajs/framework/types"

const PersonalizedOrderItemsWidget = ({ 
  data: order,
}: DetailWidgetProps<AdminOrder>) => {
  const items = order.items.filter((item) => {
    return item.variant?.product?.metadata?.is_personalized
  })

  if (!items.length) {
    return <></>
  }

  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">Personalized Order Items</Heading>
      </div>
      <div className="divide-y">
        {items.map((item) => (
          <div key={item.id} className="flex gap-4 px-6 py-4">
            {item.variant?.product?.thumbnail && <img
              src={item.variant.product.thumbnail}
              alt={item.variant.title || "Personalized Product"}
              className="h-8 w-6 object-cover rounded border border-ui-border"
            />}
            <div className="flex flex-col">
              <Text size="small" weight="plus">
                {item.variant?.product?.title}: {item.variant?.title}
              </Text>
              <Text size="small" className="text-ui-fg-subtle">
                Width (cm): {item.metadata?.width as number || "N/A"}
              </Text>
              <Text size="small" className="text-ui-fg-subtle">
                Height (cm): {item.metadata?.height as number || "N/A"}
              </Text>
            </div>
          </div>
        ))}
      </div>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "order.details.after",
})

export default PersonalizedOrderItemsWidget
```

A widget file must export:

- A default React component. This component renders the widget's UI.
- A `config` object created with the `defineWidgetConfig` function. It accepts an object with the `zone` property that indicates where the widget will be rendered in the Medusa Admin dashboard.

The widget component accepts a `data` prop that contains the order data.

In the component, you retrieve the items whose product is personalized. Then, you display those items with their height and width values. Remember, the `metadata` property is copied from the cart's line items to the order's line items.

If there are no personalized items in the order, you don't show the widget.

### Test the Personalized Order Items Widget

To test out the personalized order items widget, start the Medusa application and open the Medusa Admin dashboard in your browser at `http://localhost:9000/app`.

Go to Orders and click on an order that contains a personalized product. You should see the "Personalized Order Items" widget displaying the personalized items with their dimensions.

![Screenshot of Medusa Admin order details page showing a custom widget titled 'Personalized Order Items' with a fabric product entry displaying the product image, title, and dimensions 'Width (cm): 100' and 'Height (cm): 80' in a clean list format](https://res.cloudinary.com/dza7lstvk/image/upload/v1753113169/Medusa%20Resources/CleanShot_2025-07-21_at_18.52.21_2x_i7shff.png)

***

## Next Steps

You've now implemented personalized products in Medusa, allowing customers to customize product dimensions and see the calculated price in the storefront. You can expand on this feature to:

- Add more personalization options, such as text engraving.
- Implement more complex pricing calculations based on additional metadata.
- Create a [custom fulfillment provider](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) to handle personalized products differently during fulfillment.

### Learn More about Medusa

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.


# Personalized Products Recipe

This recipe provides the general steps to build personalized products in Medusa.

[Personalized Products Example](https://docs.medusajs.com/recipes/personalized-products/example/index.html.md): Find a complete example of personalized products in Medusa.

## Overview

Personalized products are products that customers can customize based on their need. For example, they can upload an image to print on a shirt or provide a message to include in a letter.

In Medusa, you can create a custom module defining data models related to personalization with the service to manage them. Then, you can link those data models to existing data models, such as products.

You also have freedom in how you choose to implement the storefront, allowing you to build a unique experience around your products.

***

## Store Personalized Data

The Cart Module's `LineItem` data model has a `metadata` property that holds any custom data. You can pass the customer's customization in the request body's `metadata` field when adding a product to the cart.

For example, if you’re asking customers to enter a message to put in a letter they’re purchasing, use the `metadata` attribute of the `LineItem` data model to set the personalized information entered by the customer:

```bash
curl -X POST '{backend_url}/store/carts/{id}/line-items' \
-H 'Content-Type: application/json' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
--data-raw '{
  "variant_id": "variant_123",
  "quantity": 1,
  "metadata": {
    "message": "Hello, World!"
  }
}'
```

Two line items in the cart having different `metadata` attributes are not considered the same item. So, each line item is managed separately and can have its own quantity.

In more complex cases, you can create a custom module that stores and manages the personalization data models. You can also create a link between these data models and the `LineItem` data model.

- [Create a Module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md): Learn how to create a module.
- [Define Module Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md): Define link between data models.

***

## Build a Custom Storefront

Medusa's modular architecture removes any restrictions on the your storefront's tech stack, design, and the experience you provide customers. The storefront connects to the Medusa application using the Store API Routes.

You can build a unique experience around your products that focuses on the customer’s personalization capabilities.

Medusa provides a Next.js Starter Storefront with basic ecommerce functionalities that can be customized. You can also build your own storefront and use Medusa’s client libraries or Store API Routes to communicate with the Medusa application.

- [Next.js Starter Storefront](https://docs.medusajs.com/nextjs-starter/index.html.md): Learn how to install the Next.js Starter Storefront.
- [Storefront Development](https://docs.medusajs.com/storefront-development/index.html.md): Find guides to build your own storefront.

***

## Pass Personalized Data to the Order

If you store the personalized data using a custom module, you can:

- Create a workflow that handles saving the personalization data.
- Create a custom API Route that executes the workflow.
- Call that API Route from the storefront after adding the item to the cart.
- Consume the `orderCreated` hook of the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) to attach the personalized data to the Order Module's `LineItem` data model.

- [Create a Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md): Learn how to create a workflow.
- [Create API Route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md): Learn how to create an API route.
- [Consume a Hook](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md): Learn how to create a hook.

***

## Fulfill Personalized Products in Orders

The Fulfillment Module handles all logic related to fulfilling orders. It also supports using fulfillment module providers that implement the logic of fulfilling orders with third-party services.

To fulfill your personalized products with a third-party service or custom logic, you can create a fulfillment module provider.

The Fulfillment Module registers your fulfillment module provider to use it to fulfill orders.

- [Fulfillment Module](https://docs.medusajs.com/commerce-modules/fulfillment/index.html.md): Learn about the Fulfillment Module.
- [Create Fulfillment Module Provider](https://docs.medusajs.com/references/fulfillment/provider/index.html.md): Learn how to create a fulfillment module provider.


# Point-of-Sale (POS) Recipe

This recipe provides the general steps to build a Point of Sale (POS) system with Medusa.

## Overview

Building a POS system on top of an ecommerce engine introduces challenges related to the tech stack used, data sync across channels, and feature availability relevant to offline sales in a POS, not just online sales.

Medusa's modular architecture solves these challenges. Any frontend can utilize Medusa's commerce features, such as sales channels or multi-warehouse features, through its REST APIs.

[How Tekla built a POS system with Medusa](https://medusajs.com/blog/tekla-agilo-pos-case/).

***

## Freedom in Choosing Your POS Tech Stack

When you build a POS system, you choose the programming language or tool you want to use.

Medusa's modular architecture removes any restrictions you may have while making this choice. Any client or frontend can connect to the Medusa application using its headless REST APIs.

![POS Tech Stack](https://res.cloudinary.com/dza7lstvk/image/upload/v1709034046/Medusa%20Book/pos-tech-stack_fy8uiu.jpg)

[Admin REST APIs](https://docs.medusajs.com/api/admin): Check out available Admin REST APIs in Medusa.

***

## Integrate a Barcode Scanner

POS systems make the checkout process smoother by integrating a barcode scanner. Merchants scan a product by its barcode to check its details or add it to the customer's purchase.

The Product Module's [ProductVariant](https://docs.medusajs.com/references/product/models/ProductVariant/index.html.md) data model has the properties to implement this integration, mainly the `barcode` attribute. Other notable properties include `ean`, `upc`, and `hs_code`, among others.

To search through product variants by their barcode, create a custom API Route and call it within your POS.

![Example flow of integrating a barcode scanner](https://res.cloudinary.com/dza7lstvk/image/upload/v1709034282/Medusa%20Book/pos-scan-barcode_a8j8ew.jpg)

- [Product Module](https://docs.medusajs.com/commerce-modules/product/index.html.md): Learn about the Product Module.
- [Create API Route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md): Learn how to create an API Route.

***

## Access Accurate Inventory Details

As you manage an online and offline store, it's essential to separate inventory quantity across different locations.

Medusa's [Inventory](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md), [Stock Location](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md), and [Sales Channel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md) modules allow merchants to manage the inventory items and their availability across locations and sales channels.

![Using Multi-warehouse features with POS](https://res.cloudinary.com/dza7lstvk/image/upload/v1709034731/Medusa%20Book/pos-multiwarehouse_r1z48x.jpg)

Merchants can create a sales channel for their online store and a sales channel for their POS system, then manage the inventory quantity of product variants in each channel.

This also opens the door for other business opportunities, such as an endless aisle experience.

Suppose a product isn't available in-store but is available in different warehouses. You can allow customers to purchase that item in-store and deliver it to their address.

- [Inventory Module](https://docs.medusajs.com/commerce-modules/inventory/index.html.md): Learn about the Inventory Module.
- [Stock Location Module](https://docs.medusajs.com/commerce-modules/stock-location/index.html.md): Learn about the Stock Location Module.

[Sales Channel Module](https://docs.medusajs.com/commerce-modules/sales-channel/index.html.md): Learn about the Sales Channel Module.

***

## Build an Omni-channel Customer Experience

Using Medusa's [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md), you can retrieve a customer's details from the Medusa application and place an order on the POS under their account. The customer can then view their order details on their online profile as if they had placed the order online.

In addition, using Medusa's [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md), store operators can create promotions on the fly for customers using the POS system and apply them to their orders.

You can also create custom modules to provide features for a better customer experience, such as a rewards system or loyalty points.

- [Loyalty Points Tutorial](https://docs.medusajs.com/how-to-tutorials/tutorials/loyalty-points/index.html.md): Learn how to create a loyalty points system.
- [Customer Module](https://docs.medusajs.com/commerce-modules/customer/index.html.md): Learn about the Customer Module.
- [Promotion Module](https://docs.medusajs.com/commerce-modules/promotion/index.html.md): Learn about the Promotion Module.
- [Create a Module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md): Learn how to create a module.

***

## Accept Payment, Place Order, and Use RMA Features

Medusa's architecture allows you to integrate any third-party payment provider for your POS and online storefront. For example, you can integrate [Stripe Terminal](https://stripe.com/terminal) to accept in-store payments.

Once you accept the payment, you can place an order in the POS system using the [Draft Order APIs](https://docs.medusajs.com/api/admin#draft-orders). Draft orders provide similar features to an online checkout experience, including discounts, payment processing, and more.

Then, merchants can view all orders coming from different sales channels using the Medusa Admin. This keeps logistics and order handling consistent between online and in-store customers.

- [Create a Payment Module Provider](https://docs.medusajs.com/references/payment/provider/index.html.md): Learn how to create a payment module provider.
- [Admin REST APIs](https://docs.medusajs.com/api/admin): Check out available Admin REST APIs.


# Subscriptions Recipe

In this guide, you'll learn how to support subscription purchases with Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with support for customizations. While Medusa doesn't provide subscription-based purchases natively, it provides the Framework to support you in implementing this feature.

In this guide, you'll customize Medusa to implement subscription-based purchases with the following features:

1. Subscription-based purchases for a specified interval (monthly or yearly) and period.
2. Customize the admin dashboard to view subscriptions and associated orders.
3. Automatic renewal of the subscription.
4. Automatic subscription expiration tracking.
5. Allow customers to view and cancel their subscriptions.

This guide uses Stripe as an example to capture the subscription payments. You're free to use a different payment provider or implement your payment logic instead.

This guide provides an example of an approach to implement subscriptions. You're free to choose a different approach using the Medusa Framework.

- [Subscription Example Repository](https://github.com/medusajs/examples/tree/main/subscription): Find the full code for this recipe example in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1721125608/OpenApi/Subscriptions_OpenApi_b371x4.yml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when you're asked whether you want to install the Next.js Starter Storefront, choose `Y` for yes.

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form.

Afterwards, you can log in with the new user and explore the dashboard. The Next.js Starter Storefront is also running at `http://localhost:8000`.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Configure Stripe Module Provider

As mentioned in the introduction, you'll use Stripe as the payment provider for the subscription payments. In this step, you'll configure the [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) in your Medusa application.

### Prerequisites

- [Stripe account](https://stripe.com/)
- [Stripe Secret API Key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard)

To add the Stripe Module Provider to the Medusa configurations, add the following to the `medusa-config.ts` file:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/payment",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/payment-stripe",
            id: "stripe",
            options: {
              apiKey: process.env.STRIPE_API_KEY,
            },
          },
        ],
      },
    },
  ],
})
```

The Medusa configurations accept a `modules` property to add modules to your application. You'll learn more about modules in the next section.

You add the Stripe Module Provider to the [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md)'s options. Learn more about these options in the [Payment Module's options documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options/index.html.md).

You also pass an `apiKey` option to the Stripe Module Provider and set its value to an environment variable. So, add the following to your `.env` file:

```plain
STRIPE_API_KEY=sk_test_51J...
```

Where `sk_test_51J...` is your Stripe Secret API Key.

Learn more about other Stripe options and configurations in the [Stripe Module Provider documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md).

### Enable Stripe in Regions

To allow customers to use Stripe during checkout, you must enable it in at least one region. Customers can only choose from payment providers available in their region. You can enable the payment provider in the Medusa Admin dashboard.

To do that, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the dashboard at `localhost:9000/app`. After you log in:

1. Go to Settings -> Regions.
2. Click on a region to edit.
3. Click on the <InlineIcon Icon={EllipsisHorizontal} alt="three-dots" /> icon at the top right of the first section.
4. In the side window that opens, edit the region's payment provider to choose "Stripe (STRIPE)".
5. Once you're done, click the Save button.

![Choose "Stripe (STRIPE)" in the payment provider dropdown](https://res.cloudinary.com/dza7lstvk/image/upload/v1740654408/Medusa%20Resources/Screenshot_2025-02-27_at_1.06.12_PM_esql1c.png)

***

## Step 3: Create Subscription Module

Medusa creates commerce features in modules. For example, product features and data models are created in the Product Module.

You also create custom commerce data models and features in custom modules. They're integrated into the Medusa application similar to Medusa's modules without side effects.

So, you'll create a subscription module that holds the data models related to a subscription and allows you to manage them.

Create the directory `src/modules/subscription`.

### Create Data Models

Create the file `src/modules/subscription/models/subscription.ts` with the following content:

```ts title="src/modules/subscription/models/subscription.ts" highlights={subscriptionHighlights}
import { model } from "@medusajs/framework/utils"
import { SubscriptionInterval, SubscriptionStatus } from "../types"

const Subscription = model.define("subscription", {
  id: model.id().primaryKey(),
  status: model.enum(SubscriptionStatus)
    .default(SubscriptionStatus.ACTIVE),
  interval: model.enum(SubscriptionInterval),
  period: model.number(),
  subscription_date: model.dateTime(),
  last_order_date: model.dateTime(),
  next_order_date: model.dateTime().index().nullable(),
  expiration_date: model.dateTime().index(),
  metadata: model.json().nullable(),
})

export default Subscription
```

This creates a `Subscription` data model that holds a subscription’s details, including:

- `interval`: indicates whether the subscription is renewed monthly or yearly.
- `period`: a number indicating how many months/years before a new order is created for the subscription. For example, if `period` is `3` and `interval` is `monthly`, then a new order is created every three months.
- `subscription_date`: when the subscription was created.
- `last_order_date`: when the last time a new order was created for the subscription.
- `next_order_date` : when the subscription’s next order should be created. This property is nullable in case the subscription doesn’t have a next date or has expired.
- `expiration_date`: when the subscription expires.
- `metadata`: any additional data can be held in this JSON property.

Notice that the data models use enums defined in another file. So, create the file `src/modules/subscription/types/index.ts` with the following content:

```ts title="src/modules/subscription/types/index.ts"
export enum SubscriptionStatus {
  ACTIVE = "active",
  CANCELED = "canceled",
  EXPIRED = "expired",
  FAILED = "failed"
}

export enum SubscriptionInterval {
  MONTHLY = "monthly",
  YEARLY = "yearly"
}
```

### Create Main Service

Create the module’s main service in the file `src/modules/subscription/service.ts` with the following content:

```ts title="src/modules/subscription/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import Subscription from "./models/subscription"

class SubscriptionModuleService extends MedusaService({
  Subscription,
}) {
}

export default SubscriptionModuleService
```

The main service extends the service factory to provide data-management features on the `Subscription` data model.

### Create Module Definition File

Create the file `src/modules/subscription/index.ts` that holds the module’s definition:

```ts title="src/modules/subscription/index.ts"
import { Module } from "@medusajs/framework/utils"
import SubscriptionModuleService from "./service"

export const SUBSCRIPTION_MODULE = "subscriptionModuleService"

export default Module(SUBSCRIPTION_MODULE, {
  service: SubscriptionModuleService,
})
```

This sets the module’s name to `subscriptionModuleService` and its main service to `SubscriptionModuleService`.

### Register Module in Medusa’s Configuration

Finally, add the module into `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    // ...
    {
      resolve: "./src/modules/subscription",
    },
  ],
})
```

### Further Read

- [How to Create a Module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md)
- [How to Create a Data Model](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md)
- [Learn more about the service factory](https://docs.medusajs.com/docs/learn/fundamentals/modules/service-factory/index.html.md).

***

## Step 4: Define Links

Modules are isolated in Medusa, making them reusable, replaceable, and integrable in your application without side effects.

So, you can't have relations between data models in modules. Instead, you define a link between them.

Links are relations between data models of different modules that maintain the isolation between the modules.

In this step, you’ll define links between the Subscription Module’s `Subscription` data model and the data models of Medusa’s Commerce Modules:

1. Link between the `Subscription` data model and the Cart Module's `Cart` model.
2. Link between the `Subscription` data model and the Customer Module's `Customer` model.
3. Link between the `Subscription` data model and the Order Module's `Order` model.

### Define a Link to Cart

To link a subscription to the cart used to make the purchase, create the file `src/links/subscription-cart.ts` with the following content:

```ts title="src/links/subscription-cart.ts"
import { defineLink } from "@medusajs/framework/utils"
import SubscriptionModule from "../modules/subscription"
import CartModule from "@medusajs/medusa/cart"

export default defineLink(
  SubscriptionModule.linkable.subscription,
  CartModule.linkable.cart
)
```

This defines a link between the `Subscription` data model and the Cart Module’s `Cart` data model.

When you create a new order for the subscription, you’ll retrieve the linked cart and use the same shipping and payment details the customer supplied when the purchase was made.

### Define a Link to Customer

To link a subscription to the customer who purchased it, create the file `src/links/subscription-customer.ts` with the following content:

```ts title="src/links/subscription-customer.ts"
import { defineLink } from "@medusajs/framework/utils"
import SubscriptionModule from "../modules/subscription"
import CustomerModule from "@medusajs/medusa/customer"

export default defineLink(
  {
    linkable: SubscriptionModule.linkable.subscription.id,
    isList: true,
  },
  CustomerModule.linkable.customer
)
```

This defines a list link to the `Subscription` data model, since a customer may have multiple subscriptions.

### Define a Link to Order

To link a subscription to the orders created for it, create the file `src/links/subscription-order.ts` with the following content:

```ts title="src/links/subscription-order.ts"
import { defineLink } from "@medusajs/framework/utils"
import SubscriptionModule from "../modules/subscription"
import OrderModule from "@medusajs/medusa/order"

export default defineLink(
  SubscriptionModule.linkable.subscription,
  {
    linkable: OrderModule.linkable.order.id,
    isList: true,
  }
)
```

This defines a list link to the `Order` data model since a subscription has multiple orders.

### Further Reads

- [How to Define a Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md)

***

## Step 5: Run Migrations

To create a table for the `Subscription` data model in the database, start by generating the migrations for the Subscription Module with the following command:

```bash
npx medusa db:generate subscriptionModuleService
```

This generates a migration in the `src/modules/subscriptions/migrations` directory.

Then, to reflect the migration and links in the database, run the following command:

```bash
npx medusa db:migrate
```

***

## Step 6: Override createSubscriptions Method in Service

Since the Subscription Module’s main service extends the service factory, it has a generic `createSubscriptions` method that creates one or more subscriptions.

In this step, you’ll override it to add custom logic to the subscription creation that sets its date properties.

### Install moment Library

Before you start, install the [Moment.js library](https://momentjs.com/) to help manipulate and format dates with the following command:

```bash npm2yarn
npm install moment
```

### Add getNextOrderDate Method

In `src/modules/subscription/service.ts`, add the following method to `SubscriptionModuleService`:

```ts title="src/modules/subscription/service.ts" highlights={getNextOrderDateHighlights}
// ...
import moment from "moment"
import { 
  CreateSubscriptionData, 
  SubscriptionData, 
  SubscriptionInterval,
} from "./types"

class SubscriptionModuleService extends MedusaService({
  Subscription,
}) {
  getNextOrderDate({
    last_order_date,
    expiration_date,
    interval,
    period,
  }: {
    last_order_date: Date
    expiration_date: Date
    interval: SubscriptionInterval,
    period: number
  }): Date | null {
    const nextOrderDate = moment(last_order_date)
      .add(
        period, 
        interval === SubscriptionInterval.MONTHLY ? 
          "month" : "year"
      )
    const expirationMomentDate = moment(expiration_date)

    return nextOrderDate.isAfter(expirationMomentDate) ? 
      null : nextOrderDate.toDate()
  }
}
```

This method accepts a subscription’s last order date, expiration date, interval, and period, and uses them to calculate and return the next order date.

If the next order date, calculated from the last order date, exceeds the expiration date, `null` is returned.

### Add getExpirationDate Method

In the same file, add the following method to `SubscriptionModuleService`:

```ts title="src/modules/subscription/service.ts"
class SubscriptionModuleService extends MedusaService({
  Subscription,
}) {
  // ...
  getExpirationDate({
    subscription_date,
    interval,
    period,
  }: {
    subscription_date: Date,
    interval: SubscriptionInterval,
    period: number
  }) {
    return moment(subscription_date)
      .add(
        period,
        interval === SubscriptionInterval.MONTHLY ?
          "month" : "year"
      ).toDate()
  }
}
```

The `getExpirationDate` method accepts a subscription’s date, interval, and period to calculate and return its expiration date.

### Override the createSubscriptions Method

Before overriding the `createSubscriptions` method, add the following types to `src/modules/subscription/types/index.ts`:

```ts title="src/modules/subscription/types/index.ts"
import { InferTypeOf } from "@medusajs/framework/types"
import Subscription from "../models/subscription"

// ...

export type CreateSubscriptionData = {
  interval: SubscriptionInterval
  period: number
  status?: SubscriptionStatus
  subscription_date?: Date
  metadata?: Record<string, unknown>
}

export type SubscriptionData = InferTypeOf<typeof Subscription>
```

Since the `Subscription` data model is a variable, use `InferTypeOf` to infer its type.

Then, in `src/modules/subscription/service.ts`, add the following to override the `createSubscriptions` method:

```ts title="src/modules/subscription/service.ts"
class SubscriptionModuleService extends MedusaService({
  Subscription,
}) {
  // ...
    
  // @ts-expect-error
  async createSubscriptions(
    data: CreateSubscriptionData | CreateSubscriptionData[]
  ): Promise<SubscriptionData[]> {
    const input = Array.isArray(data) ? data : [data]

    const subscriptions = await Promise.all(
      input.map(async (subscription) => {
        const subscriptionDate = subscription.subscription_date || new Date()
        const expirationDate = this.getExpirationDate({
          subscription_date: subscriptionDate,
          interval: subscription.interval,
          period: subscription.period,
        })

        return await super.createSubscriptions({
          ...subscription,
          subscription_date: subscriptionDate,
          last_order_date: subscriptionDate,
          next_order_date: this.getNextOrderDate({
            last_order_date: subscriptionDate,
            expiration_date: expirationDate,
            interval: subscription.interval,
            period: subscription.period,
          }),
          expiration_date: expirationDate,
        })
      })
    )
    
    return subscriptions
  }
}
```

The `createSubscriptions` calculates for each subscription the expiration and next order dates using the methods created earlier. It creates and returns the subscriptions.

This method is used in the next step.

***

## Step 7: Create Subscription Workflow

To implement and expose a feature that manipulates data, you create a workflow that uses services to implement the functionality, then create an API route that executes that workflow.

In this step, you’ll create the workflow that you’ll execute when a customer purchases a subscription.

The workflow accepts a cart’s ID, and it has three steps:

1. Create the order from the cart.
2. Create a subscription.
3. Link the subscription to the order, cart, and customer.

Medusa provides the first and last steps in the `@medusajs/medusa/core-flows` package, so you only need to implement the second step.

### Create a Subscription Step (Second Step)

Create the file `src/workflows/create-subscription/steps/create-subscription.ts` with the following content:

```ts title="src/workflows/create-subscription/steps/create-subscription.ts" highlights={createSubscriptionsHighlights} collapsibleLines="1-7" expandMoreLabel="Show Imports"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
import { LinkDefinition } from "@medusajs/framework/types"
import { SubscriptionInterval } from "../../../modules/subscription/types"
import SubscriptionModuleService from "../../../modules/subscription/service"
import { SUBSCRIPTION_MODULE } from "../../../modules/subscription"

type StepInput = {
  cart_id: string
  order_id: string
  customer_id?: string
  subscription_data: {
    interval: SubscriptionInterval
    period: number
  }
}

const createSubscriptionStep = createStep(
  "create-subscription",
  async ({ 
    cart_id, 
    order_id, 
    customer_id,
    subscription_data,
  }: StepInput, { container }) => {
    const subscriptionModuleService: SubscriptionModuleService = 
      container.resolve(SUBSCRIPTION_MODULE)
    const linkDefs: LinkDefinition[] = []

    const subscription = await subscriptionModuleService.createSubscriptions({
      ...subscription_data,
      metadata: {
        main_order_id: order_id,
      },
    })

    // TODO add links

    return new StepResponse({
      subscription: subscription[0],
      linkDefs,
    }, {
      subscription: subscription[0],
    })
  }, async (data, { container }) => {
    // TODO implement compensation
  }
)

export default createSubscriptionStep
```

This step receives the IDs of the cart, order, and customer, along with the subscription’s details.

In this step, you use the `createSubscriptions` method to create the subscription. In the `metadata` property, you set the ID of the order created on purchase.

The step returns the created subscription as well as an array of links to create. To add the links to be created in the returned array, replace the first `TODO` with the following:

```ts title="src/workflows/create-subscription/steps/create-subscription.ts" highlights={createSubscriptionsLinkHighlights}
linkDefs.push({
  [SUBSCRIPTION_MODULE]: {
    "subscription_id": subscription[0].id,
  },
  [Modules.ORDER]: {
    "order_id": order_id,
  },
})

linkDefs.push({
  [SUBSCRIPTION_MODULE]: {
    "subscription_id": subscription[0].id,
  },
  [Modules.CART]: {
    "cart_id": cart_id,
  },
})

if (customer_id) {
  linkDefs.push({
    [SUBSCRIPTION_MODULE]: {
      "subscription_id": subscription[0].id,
    },
    [Modules.CUSTOMER]: {
      "customer_id": customer_id,
    },
  })
}
```

This adds links between:

1. The subscription and the order.
2. The subscription and the cart.
3. The subscription and the customer, if a customer is associated with the cart.

The step also has a compensation function to undo the step’s changes if an error occurs. So, replace the second `TODO` with the following:

```ts title="src/workflows/create-subscription/steps/create-subscription.ts"
if (!data) {
  return
}
const subscriptionModuleService: SubscriptionModuleService = 
  container.resolve(SUBSCRIPTION_MODULE)

await subscriptionModuleService.cancelSubscriptions(data.subscription.id)
```

The compensation function receives the subscription as a parameter. It cancels the subscription.

### Create Workflow

Create the file `src/workflows/create-subscription/index.ts` with the following content:

```ts title="src/workflows/create-subscription/index.ts" highlights={createSubscriptionWorkflowHighlights} collapsibleLines="1-13" expandMoreLabel="Show Imports"
import { 
  createWorkflow,
  WorkflowResponse,
  useQueryGraphStep,
  acquireLockStep,
  releaseLockStep,
} from "@medusajs/framework/workflows-sdk"
import { 
  createRemoteLinkStep,
  completeCartWorkflow,
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { 
  SubscriptionInterval,
} from "../../modules/subscription/types"
import createSubscriptionStep from "./steps/create-subscription"

type WorkflowInput = {
  cart_id: string,
  subscription_data: {
    interval: SubscriptionInterval
    period: number
  }
}

const createSubscriptionWorkflow = createWorkflow(
  "create-subscription",
  (input: WorkflowInput) => {
    acquireLockStep({
      key: input.cart_id,
      timeout: 2,
      ttl: 10,
    })
    const { id } = completeCartWorkflow.runAsStep({
      input: {
        id: input.cart_id,
      },
    })

    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "id",
        "status",
        "summary",
        "currency_code",
        "customer_id",
        "display_id",
        "region_id",
        "email",
        "total",
        "subtotal",
        "tax_total",
        "discount_total",
        "discount_subtotal",
        "discount_tax_total",
        "original_total",
        "original_tax_total",
        "item_total",
        "item_subtotal",
        "item_tax_total",
        "original_item_total",
        "original_item_subtotal",
        "original_item_tax_total",
        "shipping_total",
        "shipping_subtotal",
        "shipping_tax_total",
        "original_shipping_tax_total",
        "original_shipping_subtotal",
        "original_shipping_total",
        "created_at",
        "updated_at",
        "credit_lines.*",
        "items.*",
        "items.tax_lines.*",
        "items.adjustments.*",
        "items.detail.*",
        "items.variant.*",
        "items.variant.product.*",
        "shipping_address.*",
        "billing_address.*",
        "shipping_methods.*",
        "shipping_methods.tax_lines.*",
        "shipping_methods.adjustments.*",
        "payment_collections.*",
      ],
      filters: {
        id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const { data: existingLinks } = useQueryGraphStep({
      entity: subscriptionOrderLink.entryPoint,
      fields: ["subscription.id"],
      filters: { order_id: orders[0].id },
    }).config({ name: "retrieve-existing-links" })

    const subscription = when(
      "create-subscription-condition",
      { existingLinks },
      (data) => data.existingLinks.length === 0
    )
    .then(() => {

      const { subscription, linkDefs } = createSubscriptionStep({
        cart_id: input.cart_id,
        order_id: orders[0].id,
        customer_id: orders[0].customer_id!,
        subscription_data: input.subscription_data,
      })
  
      createRemoteLinkStep(linkDefs)

      return subscription
    })

    releaseLockStep({
      key: input.cart_id,
    })

    return new WorkflowResponse({
      subscription: subscription,
      order: orders[0],
    })
  }
)

export default createSubscriptionWorkflow
```

This workflow accepts the cart’s ID, along with the subscription details. It executes the following steps:

1. [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md) to acquire a lock on the cart to prevent race conditions.
2. [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) that completes a cart and creates an order.
3. [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md) to retrieve the order's details. [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) is a tool that allows you to retrieve data across modules.
4. [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md) again to check if a subscription already exists for the order. This is necessary to ensure idempotency in case the workflow is retried.
5. Use `when` to check if a subscription already exists for the order. If not, it executes the next steps:
   1. `createSubscriptionStep`, which is the step you created previously.
   2. `createRemoteLinkStep` which accepts links to create. These links are in the `linkDefs` array returned by the previous step.
6. [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md) to release the lock on the cart.

The workflow returns the created subscription and order.

### Further Reads

- [How to Create a Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md)
- [Learn more about the compensation function](https://docs.medusajs.com/docs/learn/fundamentals/workflows/compensation-function/index.html.md)
- [How to use Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md)

***

## Step 8: Custom Complete Cart API Route

To create a subscription when a customer completes their purchase, you need to expose an endpoint that executes the subscription workflow. To do that, you'll create an API route.

An [API Route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) is an endpoint that exposes commerce features to external applications and clients, such as storefronts.

In this step, you’ll create a custom API route similar to the [Complete Cart API route](https://docs.medusajs.com/api/store#carts_postcartsidcomplete) that uses the workflow you previously created to complete the customer's purchase and create a subscription.

Create the file `src/api/store/carts/[id]/subscribe/route.ts` with the following content:

```ts title="src/api/store/carts/[id]/subscribe/route.ts" highlights={completeCartHighlights} collapsibleLines="1-10" expandMoreLabel="Show Imports"
import { 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { 
  ContainerRegistrationKeys,
  MedusaError,
} from "@medusajs/framework/utils"
import createSubscriptionWorkflow from "../../../../../workflows/create-subscription"
import { SubscriptionInterval } from "../../../../../modules/subscription/types"

export const POST = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: [cart] } = await query.graph({
    entity: "cart",
    fields: [
      "metadata",
    ],
    filters: {
      id: [req.params.id],
    },
  })
  
  const { metadata } = cart

  if (!metadata?.subscription_interval || !metadata.subscription_period) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "Please set the subscription's interval and period first."
    )
  }

  const { result } = await createSubscriptionWorkflow(
    req.scope
  ).run({
    input: {
      cart_id: req.params.id,
      subscription_data: {
        interval: metadata.subscription_interval as SubscriptionInterval,
        period: metadata.subscription_period as number,
      },
    },
  })

  res.json({
    type: "order",
    order: result.order,
  })
}
```

Since the file exports a `POST` function, you're exposing a `POST` API route at `/store/carts/[id]/subscribe`.

In the route handler function, you retrieve the cart to access it's `metadata` property. If the subscription details aren't stored there, you throw an error.

Then, you use the `createSubscriptionWorkflow` you created to create the order, and return the created order in the response.

In the next step, you'll customize the Next.js Starter Storefront, allowing you to test out the subscription feature.

### Further Reads

- [How to Create an API Route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md)

***

## Intermission: Payment Flow Overview

Before continuing with the customizations, you must understand the payment changes you need to make to support subscriptions. In this guide, you'll get a general overview, but you can refer to the [Payment in Storefront documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/payment/index.html.md) for more details.

By default, the checkout flow requires you to create a [payment collection](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-collection/index.html.md), then a [payment session](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-session/index.html.md) in that collection. When you create the payment session, that subsequently performs the necessary action to initialize the payment in the payment provider. For example, it creates a payment intent in Stripe.

![Diagram showcasing payment flow overview](https://res.cloudinary.com/dza7lstvk/image/upload/v1740657136/Medusa%20Resources/subscriptions-stripe_ycw4ig.jpg)

To support subscriptions, you need to support capturing the payment each time the subscription renews. When creating the payment session using the [Initialize Payment Session API route](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions), you must pass the data that your payment provider requires to support capturing the payment again in the future. You can pass the data that the provider requires in the `data` property.

If you're using a custom payment provider, you can handle that additional data in the [initiatePayment method](https://docs.medusajs.com/references/payment/provider#initiatepayment/index.html.md) of your provider's service.

When you create the payment session, Medusa creates an account holder for the customer. An account holder represents a customer's saved payment information, including saved methods, in a third-party provider and may hold data from that provider. Learn more in the [Account Holder](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/account-holder/index.html.md) documentation.

The account holder allows you to retrieve the saved payment method and use it to capture the payment when the subscription renews. You'll see how this works later when you implement the logic to renew the subscription.

***

## Step 9: Add Subscriptions to Next.js Starter Storefront

In this step, you'll customize the checkout flow in the [Next.js Starter storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), which you installed in the first step, to:

1. Add a subscription step to the checkout flow.
2. Pass the additional data that Stripe requires to later capture the payment when the subscription renews, as explained in the [Payment Flow Overview](#intermission-payment-flow-overview).
3. Change the complete cart action to use the custom API route you created in the previous step.

### Add Subscription Step

Start by adding the function to update the subscription data in the cart. Add to the file `src/lib/data/cart.ts` the following:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="orange"
export enum SubscriptionInterval {
  MONTHLY = "monthly",
  YEARLY = "yearly"
}

export async function updateSubscriptionData(
  subscription_interval: SubscriptionInterval,
  subscription_period: number
) {
  const cartId = getCartId()
  
  if (!cartId) {
    throw new Error("No existing cart found when placing an order")
  }

  await updateCart({
    metadata: {
      subscription_interval,
      subscription_period,
    },
  })
  revalidateTag("cart")
}
```

This updates the cart's `metadata` with the subscription details.

Then, you'll add the subscription form that shows as part of the checkout to select the subscription interval and period. Create the file `src/modules/checkout/components/subscriptions/index.tsx` with the following content:

```tsx title="src/modules/checkout/components/subscriptions/index.tsx" badgeLabel="Storefront" badgeColor="orange" collapsibleLines="1-12" expandMoreLabel="Show Imports"
"use client"

import { Button, clx, Heading, Text } from "@medusajs/ui"
import { CheckCircleSolid } from "@medusajs/icons"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { useCallback, useState } from "react"
import Divider from "../../../common/components/divider"
import Input from "../../../common/components/input"
import NativeSelect from "../../../common/components/native-select"
import { capitalize } from "lodash"
import { updateSubscriptionData } from "../../../../lib/data/cart"

export enum SubscriptionInterval {
  MONTHLY = "monthly",
  YEARLY = "yearly"
}

const SubscriptionForm = () => {
  const [interval, setInterval] = useState<SubscriptionInterval>(
    SubscriptionInterval.MONTHLY
  )
  const [period, setPeriod] = useState(1)
  const [isLoading, setIsLoading] = useState(false)

  const searchParams = useSearchParams()
  const router = useRouter()
  const pathname = usePathname()

  const isOpen = searchParams.get("step") === "subscription"

  const createQueryString = useCallback(
    (name: string, value: string) => {
      const params = new URLSearchParams(searchParams)
      params.set(name, value)

      return params.toString()
    },
    [searchParams]
  )

  const handleEdit = () => {
    router.push(pathname + "?" + createQueryString("step", "subscription"), {
      scroll: false,
    })
  }

  const handleSubmit = async () => {
    setIsLoading(true)
    
    updateSubscriptionData(interval, period)
    .then(() => {
      setIsLoading(false)
      router.push(pathname + "?step=delivery", { scroll: false })
    })
  }

  return (
    <div className="bg-white">
      <div className="flex flex-row items-center justify-between mb-6">
        <Heading
          level="h2"
          className={clx(
            "flex flex-row text-3xl-regular gap-x-2 items-baseline",
            {
              "opacity-50 pointer-events-none select-none":
                !isOpen,
            }
          )}
        >
          Subscription Details
          {!isOpen && <CheckCircleSolid />}
        </Heading>
        {!isOpen && (
          <Text>
            <button
              onClick={handleEdit}
              className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover"
              data-testid="edit-payment-button"
            >
              Edit
            </button>
          </Text>
        )}
      </div>
      <div>
        <div className={isOpen ? "block" : "hidden"}>
          <div className="flex flex-col gap-4">
            <NativeSelect 
              placeholder="Interval" 
              value={interval} 
              onChange={(e) => 
                setInterval(e.target.value as SubscriptionInterval)
              }
              required
              autoComplete="interval"
            >
              {Object.values(SubscriptionInterval).map(
                (intervalOption, index) => (
                  <option key={index} value={intervalOption}>
                    {capitalize(intervalOption)}
                  </option>
                )
              )}
            </NativeSelect>
            <Input
              label="Period"
              name="period"
              autoComplete="period"
              value={period}
              onChange={(e) => 
                setPeriod(parseInt(e.target.value))
              }
              required
              type="number"
            />
          </div>

          <Button
            size="large"
            className="mt-6"
            onClick={handleSubmit}
            isLoading={isLoading}
            disabled={!interval || !period}
          >
            Continue to delivery
          </Button>
        </div>
      </div>
      <Divider className="mt-8" />
    </div>
  )
}

export default SubscriptionForm
```

This adds a component that displays a form to choose the subscription's interval and period during checkout. When the customer submits the form, you use the `updateSubscriptionData` function that sends a request to the Medusa application to update the cart with the subscription details.

Next, you want the subscription step to show after the address step. So, change the last line of the `setAddresses` function in `src/lib/data/cart.ts` to redirect to the subscription step once the customer enters their address:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="orange"
export async function setAddresses(currentState: unknown, formData: FormData) {
  // ...
  redirect(
    `/${formData.get("shipping_address.country_code")}/checkout?step=subscription`
  )
}
```

And to show the subscription form during checkout, add the `SubscriptionForm` in `src/modules/checkout/templates/checkout-form/index.tsx` after the `Addresses` wrapper component:

```tsx title="src/modules/checkout/templates/checkout-form/index.tsx" badgeLabel="Storefront" badgeColor="orange"
// other imports...
import SubscriptionForm from "@modules/checkout/components/subscriptions"

export default async function CheckoutForm({
  cart,
  customer,
}: {
  cart: HttpTypes.StoreCart | null
  customer: HttpTypes.StoreCustomer | null
}) {
  // ...

  return (
    <div>
      {/* ... */}
      {/* After Addresses, before Shipping */}
      <div>
        <SubscriptionForm />
      </div>
      {/* ... */}
    </div>
  )
}
```

### Pass Additional Data to Payment Provider

As explained in the [Payment Flow Overview](#intermission-payment-flow-overview), you need to pass additional data to the payment provider to support subscriptions.

For Stripe, you need to pass the `setup_future_usage` property in the `data` object when you create the payment session. This property allows you to capture the payment in the future, as explained in [Stripe's documentation](https://docs.stripe.com/payments/payment-intents#future-usage).

To pass this data, you'll make changes in the `src/modules/checkout/components/payment/index.tsx` file. In this file, the `initiatePaymentSession` is used in two places. In each of them, pass the `data` property as follows:

```tsx title="src/modules/checkout/components/payment/index.tsx" badgeLabel="Storefront" badgeColor="orange"
await initiatePaymentSession(cart, {
  provider_id: method,
  data: {
    setup_future_usage: "off_session",
  },
})
```

If you're integrating with a custom payment provider, you can instead pass the required data for that provider in the `data` object.

The payment method can now be used later to capture the payment when the subscription renews.

### Change Complete Cart Action

Finally, you need to change the complete cart action to use the custom API route you created in the previous step.

In `src/lib/data/cart.ts`, find the `placeOrder` function and change the `await sdk.store.cart.complete` call to the following:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="orange"
const cartRes = await sdk.client.fetch<{
  type: "cart"
  cart: HttpTypes.StoreCart
} | {
  type: "order"
  order: HttpTypes.StoreOrder
}>(
  `/store/carts/${id}/subscribe`,
  {
    method: "POST",
    headers,
  }
)
```

You change the request to send a `POST` request to the `/store/carts/[id]/subscribe` endpoint you created earlier. You can keep the rest of the `placeOrder` function as is.

### Test Cart Completion and Subscription Creation

To test out the cart completion flow:

1. In the Medusa application's directory, run the following command to start the application:

```bash npm2yarn
npm run dev
```

2. In the Next.js Starter's directory, run the following command to start the storefront:

```bash npm2yarn
npm run dev
```

3. Add a product to the cart and place an order. During checkout, you'll see a Subscription Details step to fill out the interval and period.

***

## Step 10: Add Admin API Routes for Subscription

In this step, you’ll add two API routes for admin users:

1. One to list all subscriptions.
2. One to retrieve a subscription.

### List Subscriptions Admin API Route

The list subscriptions API route should allow clients to retrieve subscriptions with pagination. An API route can be configured to accept pagination fields, such as `limit` and `offset`, then use them with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to paginate the retrieved data.

You'll start with the API route. To create it, create the file `src/api/admin/subscriptions/route.ts` with the following content:

```ts title="src/api/admin/subscriptions/route.ts" highlights={listSubscriptionsAdminHighlight}
import { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
  
  const { 
    data: subscriptions,
    metadata: { count, take, skip } = {},
  } = await query.graph({
    entity: "subscription",
    ...req.queryConfig,
  })

  res.json({
    subscriptions,
    count,
    limit: take,
    offset: skip,
  })
}
```

This adds a `GET` API route at `/admin/subscriptions`. In the route handler, you use Query to retrieve the subscriptions. Notice that you pass the `req.queryConfig` object to the `query.graph` method. This object contains the pagination fields, such as `limit` and `offset`, which are combined from the configurations you'll add in the middleware, and the optional query parameters in the request.

Then, you return the subscriptions, along with the:

- `count`: The total number of subscriptions.
- `limit`: The maximum number of subscriptions returned.
- `offset`: The number of subscriptions skipped before retrieving the subscriptions.

These fields are useful for clients to paginate the subscriptions.

### Add Query Configuration Middleware

To configure the pagination and retrieved fields within the route handler, and to allow passing query parameters that change these configurations in the request, you need to add the `validateAndTransformQuery` [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) to the route.

To add a middleware, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import { 
  validateAndTransformQuery,
  defineMiddlewares,
} from "@medusajs/framework/http"
import { createFindParams } from "@medusajs/medusa/api/utils/validators"

export const GetCustomSchema = createFindParams()

export default defineMiddlewares({
  routes: [
    {
      matcher: "/admin/subscriptions",
      method: "GET",
      middlewares: [
        validateAndTransformQuery(
          GetCustomSchema,
          {
            defaults: [
              "id",
              "subscription_date",
              "expiration_date",
              "status",
              "metadata.*",
              "orders.*",
              "customer.*",
            ],
            isList: true,
          }
        ),
      ],
    },
  ],
})
```

You add the `validateAndTransformQuery` middleware to `GET` requests sent to routes starting with `/admin/subscriptions`. This middleware accepts the following parameters:

- A validation schema indicating which query parameters are accepted. You create the schema with [Zod](https://zod.dev/). Medusa has a createFindParams utility that generates a Zod schema accepting four query parameters:
  - `fields`: The fields and relations to retrieve in the returned resources.
  - `offset`: The number of items to skip before retrieving the returned items.
  - `limit`: The maximum number of items to return.
  - `order`: The fields to order the returned items by in ascending or descending order.
- A Query configuration object. It accepts the following properties:
  - `defaults`: An array of default fields and relations to retrieve in each resource.
  - `isList`: A boolean indicating whether a list of items are returned in the response.

The middleware combines your default configurations with the query parameters in the request to determine the fields to retrieve and the pagination settings.

Refer to the [Request Query Configuration](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#request-query-configurations/index.html.md) documentation to learn more about this middleware and the query configurations.

### Get Subscription Admin API Route

Next, you'll add the API route to retrieve a single subscription. So, create the file `src/api/admin/subscriptions/[id]/route.ts` with the following content:

```ts title="src/api/admin/subscriptions/[id]/route.ts" highlights={getSubscriptionsAdminHighlight}
import { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: [subscription] } = await query.graph({
    entity: "subscription",
    fields: [
      "*",
      "orders.*",
      "customer.*",
    ],
    filters: {
      id: [req.params.id],
    },
  })

  res.json({
    subscription,
  })
}
```

This adds a `GET` API route at `/admin/subscriptions/[id]`, where `[id]` is the ID of the subscription to retrieve.

In the route handler, you retrieve a subscription by its ID using Query and return it in the response.

You can also use Query configuration as explained for the previous route.

In the next section, you’ll extend the Medusa Admin and use these API routes to show the subscriptions.

***

## Step 11: Extend Admin

The Medusa Admin is customizable, allowing you to inject widgets into existing pages or add UI routes to create new pages.

In this step, you’ll add two UI routes:

1. One to view all subscriptions.
2. One to view a single subscription.

### Create Types File

Before creating the UI routes, create the file `src/admin/types/index.ts` that holds types used by the UI routes:

```ts title="src/admin/types/index.ts"
import { 
  OrderDTO,
  CustomerDTO,
} from "@medusajs/framework/types"

export enum SubscriptionStatus {
  ACTIVE = "active",
  CANCELED = "canceled",
  EXPIRED = "expired",
  FAILED = "failed"
}

export enum SubscriptionInterval {
  MONTHLY = "monthly",
  YEARLY = "yearly"
}

export type SubscriptionData = {
  id: string
  status: SubscriptionStatus
  interval: SubscriptionInterval
  subscription_date: string
  last_order_date: string
  next_order_date: string | null
  expiration_date: string
  metadata: Record<string, unknown> | null
  orders?: OrderDTO[]
  customer?: CustomerDTO
}

```

You define types for the subscription status and interval, as well as a `SubscriptionData` type that represents the subscription data. The `SubscriptionData` type includes the subscription's ID, status, interval, dates, metadata, and related orders and customer. You'll use these types for the subscriptions retrieved from the server.

### Configure JS SDK

Medusa provides a [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) that facilitates sending requests to the server. You can use this SDK in any JavaScript client-side application, including your admin customizations.

To configure the JS SDK, create the file `src/admin/lib/sdk.ts` with the following content:

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

This initializes the SDK, setting the following options:

- `baseUrl`: The URL of the Medusa server. You use the Vite environment variable `VITE_BACKEND_URL`.
- `debug`: A boolean indicating whether to log debug information. You use the Vite environment variable `DEV`.
- `auth`: An object indicating the authentication type. You use the session authentication type, which is the recommended approach for admin customizations.

Learn more about other customizations in the [JS SDK documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md).

### Create Subscriptions List UI Route

You'll now create the subscriptions list UI route. Since you'll show the subscriptions in a table, you'll use the [DataTable component](https://docs.medusajs.com/ui/components/data-table/index.html.md) from Medusa UI. It facilitates displaying data in a tabular format with sorting, filtering, and pagination.

Start by creating the file `src/admin/routes/subscriptions/page.tsx` with the following content:

```tsx title="src/admin/routes/subscriptions/page.tsx" highlights={list1Highlights} collapsibleLines="1-9" expandMoreLabel="Show Imports"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ClockSolid } from "@medusajs/icons"
import { Container, Heading, Badge, createDataTableColumnHelper, useDataTable, DataTablePaginationState, DataTable } from "@medusajs/ui"
import { useMemo, useState } from "react"
import { SubscriptionData, SubscriptionStatus } from "../../types"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../../lib/sdk"
import { useNavigate } from "react-router-dom"

const getBadgeColor = (status: SubscriptionStatus) => {
  switch(status) {
    case SubscriptionStatus.CANCELED:
      return "orange"
    case SubscriptionStatus.FAILED:
      return "red"
    case SubscriptionStatus.EXPIRED:
      return "grey"
    default:
      return "green"
  }
}

const getStatusTitle = (status: SubscriptionStatus) => {
  return status.charAt(0).toUpperCase() + 
    status.substring(1)
}

const columnHelper = createDataTableColumnHelper<SubscriptionData>()

const columns = [
  columnHelper.accessor("id", {
    header: "#",
  }),
  columnHelper.accessor("metadata.main_order_id", {
    header: "Main Order",
  }),
  columnHelper.accessor("customer.email", {
    header: "Customer",
  }),
  columnHelper.accessor("subscription_date", {
    header: "Subscription Date",
    cell: ({ getValue }) => {
      return getValue().toLocaleString()
    },
  }),
  columnHelper.accessor("expiration_date", {
    header: "Expiry Date",
    cell: ({ getValue }) => {
      return getValue().toLocaleString()
    },
  }),
  columnHelper.accessor("status", {
    header: "Status",
    cell: ({ getValue }) => {
      return (
        <Badge color={getBadgeColor(getValue())}>
          {getStatusTitle(getValue())}
        </Badge>
      )
    },
  }),
]

const SubscriptionsPage = () => {
  // TODO add implementation
}

export const config = defineRouteConfig({
  label: "Subscriptions",
  icon: ClockSolid,
})

export default SubscriptionsPage
```

First, you define two helper functions: `getBadgeColor` to get the color for the status badge, and `getStatusTitle` to capitalize the status for the badge text.

Then, you use the `createDataTableColumnHelper` utility to create a column helper. This utility simplifies defining columns for the data table. You define the columns for the data table using the helper, specifying the accessor, header, and cell for each column.

The UI route file must export a React component, `SubscriptionsPage`, that shows the content of the UI route. You'll implement this component in a bit.

Finally, you can export the route configuration using the `defineRouteConfig` function, which shows the UI route in the sidebar with the specified label and icon.

In the `SubscriptionsPage` component, you'll fetch the subscriptions and show them in a data table. So, replace the `SubscriptionsPage` component with the following:

```tsx title="src/admin/routes/subscriptions/page.tsx"
const SubscriptionsPage = () => {
  const navigate = useNavigate()
  const [pagination, setPagination] = useState<DataTablePaginationState>({
    pageSize: 4,
    pageIndex: 0,
  })

  const query = useMemo(() => {
    return new URLSearchParams({
      limit: `${pagination.pageSize}`,
      offset: `${pagination.pageIndex * pagination.pageSize}`,
    })
  }, [pagination])

  const { data, isLoading } = useQuery<{
    subscriptions: SubscriptionData[],
    count: number
  }>({
    queryFn: () => sdk.client.fetch(`/admin/subscriptions?${query.toString()}`),
    queryKey: ["subscriptions", query.toString()],
  })

  const table = useDataTable({
    columns,
    data: data?.subscriptions || [],
    getRowId: (subscription) => subscription.id,
    rowCount: data?.count || 0,
    isLoading,
    pagination: {
      state: pagination,
      onPaginationChange: setPagination,
    },
    onRowClick(event, row) {
      navigate(`/subscriptions/${row.id}`)
    },
  })


  return (
    <Container>
      <DataTable instance={table}>
        <DataTable.Toolbar>
          <Heading level="h1">Subscriptions</Heading>
        </DataTable.Toolbar>
				<DataTable.Table />
        {/** This component will render the pagination controls **/}
        <DataTable.Pagination />
      </DataTable>
    </Container>
  )
}
```

In the component, you first initialize a `pagination` state variable of type `DataTablePaginationState`. This is necessary for the table to manage pagination.

Then, you use the `useQuery` hook from the `@tanstack/react-query` package to fetch the subscriptions. In the query function, you use the JS SDK to send a request to the `/admin/subscriptions` API route with the pagination query parameters.

Next, you use the `useDataTable` hook to create a data table instance. You pass the columns, subscriptions data, row count, loading state, and pagination settings to the hook. You also navigate to the subscription details page when a row is clicked.

Finally, you render the data table with the subscriptions data, along with the pagination controls.

The subscriptions UI route will now show a table of subscriptions, and when you click on the ID of any of them, you can view its individual page that you'll create next.

### Create a Single Subscription UI Route

To create the UI route or page that shows the details of a single subscription, create the file `src/admin/routes/subscriptions/[id]/page.tsx` with the following content:

```tsx title="src/admin/routes/subscriptions/[id]/page.tsx"
import { 
  Container,
  Heading,
  Table,
} from "@medusajs/ui"
import { useParams, Link } from "react-router-dom"
import { SubscriptionData } from "../../../types/index.js"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../../../lib/sdk.js"

const SubscriptionPage = () => {
  const { id } = useParams()
  const { data, isLoading } = useQuery<{
    subscription: SubscriptionData
  }>({
    queryFn: () => sdk.client.fetch(`/admin/subscriptions/${id}`),
    queryKey: ["subscription", id],
  })

  return (
    <Container>
      {isLoading && <span>Loading...</span>}
      {data?.subscription && (
        <>
          <Heading level="h1">Orders of Subscription #{data.subscription.id}</Heading>
          <Table>
            <Table.Header>
              <Table.Row>
                <Table.HeaderCell>#</Table.HeaderCell>
                <Table.HeaderCell>Date</Table.HeaderCell>
                <Table.HeaderCell>View Order</Table.HeaderCell>
              </Table.Row>
            </Table.Header>
            <Table.Body>
              {data.subscription.orders?.map((order) => (
                <Table.Row key={order.id}>
                  <Table.Cell>{order.id}</Table.Cell>
                  <Table.Cell>{(new Date(order.created_at)).toDateString()}</Table.Cell>
                  <Table.Cell>
                    <Link to={`/orders/${order.id}`}>
                      View Order
                    </Link>
                  </Table.Cell>
                </Table.Row>
              ))}
            </Table.Body>
          </Table>
        </>
      )}
    </Container>
  )
}

export default SubscriptionPage
```

This creates the React component used to display a subscription’s details page. Again, you use the `useQuery` hook to fetch the subscription data using the JS SDK. You pass the subscription ID from the route parameters to the hook.

Then, you render the subscription’s orders in a table. For each order, you show the ID, date, and a link to view the order.

### Test the UI Routes

To test the UI routes, run the Medusa application and go to `http://localhost:9000/app`.

After you log-in, you’ll find a new sidebar item “Subscriptions”. Once you click on it, you’ll see the list of subscription purchases.

To view a subscription’s details, click on its row, which opens the subscription details page. This page contains the subscription’s orders.

### Further Reads

- [How to Create UI Routes](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md)
- [DataTable component](https://docs.medusajs.com/ui/components/data-table/index.html.md)

***

## Step 12: Create New Subscription Orders Workflow

In this step, you’ll create a workflow to create a new subscription order. Later, you’ll execute this workflow in a scheduled job.

The workflow has eight steps:

```mermaid
graph TD
  useQueryGraphStep["Retrieve Cart (useQueryGraphStep by Medusa)"] --> createPaymentCollectionStep["createPaymentCollectionStep (Medusa)"]
  createPaymentCollectionStep["createPaymentCollectionStep (Medusa)"] --> getPaymentMethodStep
  getPaymentMethodStep --> createPaymentSessionsWorkflow["createPaymentSessionsWorkflow (Medusa)"]
  createPaymentSessionsWorkflow["createPaymentSessionsWorkflow (Medusa)"] --> authorizePaymentSessionStep["authorizePaymentSessionStep (Medusa)"]
  authorizePaymentSessionStep["authorizePaymentSessionStep (Medusa)"] --> createSubscriptionOrderStep
  createSubscriptionOrderStep --> createRemoteLinkStep["Create Links (createRemoteLinkStep by Medusa)"]
  createRemoteLinkStep["Create Links (createRemoteLinkStep by Medusa)"] --> capturePaymentStep["capturePaymentStep (Medusa)"]
  capturePaymentStep["capturePaymentStep (Medusa)"] --> updateSubscriptionStep
```

1. Retrieve the subscription’s linked cart. Medusa provides a `useQueryGraphStep` in the `@medusajs/medusa/core-flows` package that can be used as a step.
2. Create a payment collection for the new order. Medusa provides a `createPaymentCollectionsStep` in the `@medusajs/medusa/core-flows` package that you can use.
3. Get the customer's saved payment method. This payment method will be used to charge the customer.
4. Create payment sessions in the payment collection. Medusa provides a `createPaymentSessionsWorkflow` in the `@medusajs/medusa/core-flows` package that can be used as a step.
5. Authorize the payment session. Medusa also provides the `authorizePaymentSessionStep` in the `@medusajs/medusa/core-flows` package, which can be used.
6. Create the subscription’s new order.
7. Create links between the subscription and the order using the `createRemoteLinkStep` provided in the `@medusajs/medusa/core-flows` package.
8. Capture the order’s payment using the `capturePaymentStep` provided by Medusa in the `@medusajs/medusa/core-flows` package.
9. Update the subscription’s `last_order_date` and `next_order_date` properties.

You’ll only implement the third, sixth, and ninth steps.

### Create getPaymentMethodStep (Third Step)

To charge the customer using their payment method saved in Stripe, you need to retrieve that payment method. As explained in the [Payment Flow Overview](#intermission-payment-flow-overview), you customized the storefront to pass the `setup_future_usage` option to Stripe. So, the payment method was saved in Stripe and linked to the customer's [account holder](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/account-holder/index.html.md), allowing you to retrieve it later and re-capture the payment.

To create the step, create the file `src/workflows/create-subscription-order/steps/get-payment-method.ts` with the following content:

```tsx title="src/workflows/create-subscription-order/steps/get-payment-method.ts"
import { MedusaError, Modules } from "@medusajs/framework/utils"
import { AccountHolderDTO, CustomerDTO, PaymentMethodDTO } from "@medusajs/framework/types"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

export interface GetPaymentMethodStepInput {
  customer?: CustomerDTO & {
    account_holder: AccountHolderDTO
  }
}

// Since we know we are using Stripe, we can get the correct creation date from their data.
const getLatestPaymentMethod = (paymentMethods: PaymentMethodDTO[]) => {
  return paymentMethods.sort(
    (a, b) =>
      ((b.data?.created as number) ?? 0) - ((a.data?.created as number) ?? 0)
  )[0]
}

export const getPaymentMethodStep = createStep(
  "get-payment-method",
  async ({ customer }: GetPaymentMethodStepInput, { container }) => {
    // TODO implement step
  }
)
```

You create a `getLatestPaymentMethod` function that receives an array of payment methods and returns the latest one based on the `created` date in the `data` field. This is based off of Stripe's payment method data, so if you're using a different payment provider, you may need to adjust this function.

Then, you create the `getPaymentMethodStep` that receives the customer's data and account holder as an input.

Next, you'll add the implementation of the step. Replace `getPaymentMethodStep` with the following:

```tsx title="src/workflows/create-subscription-order/steps/get-payment-method.ts"
export const getPaymentMethodStep = createStep(
  "get-payment-method",
  async ({ customer }: GetPaymentMethodStepInput, { container }) => {
    const paymentModuleService = container.resolve(Modules.PAYMENT)

    if (!customer?.account_holder) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "No account holder found for the customer while retrieving payment method"
      )
    }

    const paymentMethods = await paymentModuleService.listPaymentMethods(
      {
        // you can change to other payment provider
        provider_id: "pp_stripe_stripe",
        context: {
          account_holder: customer.account_holder,
        },
      }
    )

    if (!paymentMethods.length) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "At least one saved payment method is required for performing a payment"
      )
    }

    const paymentMethodToUse = getLatestPaymentMethod(paymentMethods)

    return new StepResponse(
      paymentMethodToUse,
      customer.account_holder
    )
  }
)
```

In the step, you first check that the customer has an account holder, and throw an error otherwise. Then, you list the customer's payment methods using the Payment Module service's `listPaymentMethods` method. You filter the payment methods to retrieve only the ones from the Stripe provider. So, if you're using a different payment provider, you may need to adjust the `provider_id` value.

If the customer doesn't have any payment methods, you throw an error. Otherwise, you return the latest payment method found using the `getLatestPaymentMethod` function.

### Create createSubscriptionOrderStep (Sixth Step)

Create the file `src/workflows/create-subscription-order/steps/create-subscription-order.ts` with the following content:

```ts title="src/workflows/create-subscription-order/steps/create-subscription-order.ts" highlights={createSubscriptionOrderStep1Highlights} collapsibleLines="1-14" expandMoreLabel="Show Imports"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { 
  CartWorkflowDTO,
  PaymentCollectionDTO,
  IOrderModuleService,
  LinkDefinition,
} from "@medusajs/framework/types"
import { 
  Modules,
} from "@medusajs/framework/utils"
import { createOrderWorkflow } from "@medusajs/medusa/core-flows"
import { SubscriptionData } from "../../../modules/subscription/types"
import { SUBSCRIPTION_MODULE } from "../../../modules/subscription"

export type CreateSubscriptionOrderStepInput = {
  subscription: SubscriptionData
  cart: CartWorkflowDTO
  payment_collection: PaymentCollectionDTO
}

function getOrderData(cart: CartWorkflowDTO) {
  // TODO format order's data
}

const createSubscriptionOrderStep = createStep(
  "create-subscription-order",
  async ({ 
    subscription, cart, payment_collection,
  }: CreateSubscriptionOrderStepInput, 
  { container, context }) => {
    const linkDefs: LinkDefinition[] = []

    const { result: order } = await createOrderWorkflow(container)
      .run({
        input: getOrderData(cart),
        context,
      })

    // TODO add links to linkDefs

    return new StepResponse({
      order,
      linkDefs,
    }, {
      order,
    })
  },
  async (data, { container }) => {
    // TODO add compensation function
  }
)

export default createSubscriptionOrderStep
```

This creates a `createSubscriptionOrderStep` that uses the `createOrdersWorkflow`, which Medusa provides in the `@medusajs/medusa/core-flows` package. The step returns the created order and an array of links to be created.

In this step, you use a `getOrderData` function to format the order’s input data.

Replace the `getOrderData` function definition with the following:

```ts title="src/workflows/create-subscription-order/steps/create-subscription-order.ts"
function getOrderData(cart: CartWorkflowDTO) {
  return {
    region_id: cart.region_id,
    customer_id: cart.customer_id,
    sales_channel_id: cart.sales_channel_id,
    email: cart.email,
    currency_code: cart.currency_code,
    shipping_address: {
      ...cart.shipping_address,
      id: null,
    },
    billing_address: {
      ...cart.billing_address,
      id: null,
    },
    items: cart.items,
    shipping_methods: cart.shipping_methods?.map((method) => ({
      name: method.name,
      amount: method.amount,
      is_tax_inclusive: method.is_tax_inclusive,
      shipping_option_id: method.shipping_option_id,
      data: method.data,
      tax_lines: method.tax_lines?.map((taxLine) => ({
        description: taxLine.description,
        tax_rate_id: taxLine.tax_rate_id,
        code: taxLine.code,
        rate: taxLine.rate,
        provider_id: taxLine.provider_id,
      })),
      adjustments: method.adjustments?.map((adjustment) => ({
        code: adjustment.code,
        amount: adjustment.amount,
        description: adjustment.description,
        promotion_id: adjustment.promotion_id,
        provider_id: adjustment.provider_id,
      })),
    })),
  }
}
```

This formats the order’s data using the cart originally used to make the subscription purchase.

Next, to add links to the returned `linkDefs` array, replace the `TODO` in the step with the following:

```ts title="src/workflows/create-subscription-order/steps/create-subscription-order.ts" highlights={createSubscriptionOrderStep2Highlights}
linkDefs.push({
  [Modules.ORDER]: {
    order_id: order.id,
  },
  [Modules.PAYMENT]: {
    payment_collection_id: payment_collection.id,
  },
},
{
  [SUBSCRIPTION_MODULE]: {
    subscription_id: subscription.id,
  },
  [Modules.ORDER]: {
    order_id: order.id,
  },
})
```

This adds links to be created into the `linkDefs` array between the new order and payment collection, and the new order and its subscription.

Finally, replace the `TODO` in the compensation function to cancel the order in case of an error:

```ts title="src/workflows/create-subscription-order/steps/create-subscription-order.ts"
if (!data) {
  return
}
const orderModuleService: IOrderModuleService = container.resolve(
  Modules.ORDER
)

await orderModuleService.cancel(data.order.id)
```

### Create updateSubscriptionStep (Ninth Step)

Before creating the seventh step, add in `src/modules/subscription/service.ts` the following new method:

```ts title="src/modules/subscription/service.ts"
class SubscriptionModuleService extends MedusaService({
  Subscription,
}) {
  // ...
  async recordNewSubscriptionOrder(id: string) {
    const subscription = await this.retrieveSubscription(id)

    const orderDate = new Date()

    return await this.updateSubscriptions({
      id,
      last_order_date: orderDate,
      next_order_date: this.getNextOrderDate({
        last_order_date: orderDate,
        expiration_date: subscription.expiration_date,
        interval: subscription.interval,
        period: subscription.period,
      }),
    })
  }
}
```

The `recordNewSubscriptionOrder` method updates a subscription’s `last_order_date` with the current date and calculates the next order date using the `getNextOrderDate` method added previously.

Then, to create the step that updates a subscription after its order is created, create the file `src/workflows/create-subscription-order/steps/update-subscription.ts` with the following content:

```ts title="src/workflows/create-subscription-order/steps/update-subscription.ts" highlights={updateSubscriptionStepHighlights} collapsibleLines="1-9" expandMoreLabel="Show Imports"
import { 
  createStep, 
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  SUBSCRIPTION_MODULE,
} from "../../../modules/subscription"
import SubscriptionModuleService from "../../../modules/subscription/service"

type StepInput = {
  subscription_id: string
}

const updateSubscriptionStep = createStep(
  "update-subscription",
  async ({ subscription_id }: StepInput, { container }) => {
    const subscriptionModuleService: SubscriptionModuleService = 
      container.resolve(
        SUBSCRIPTION_MODULE
      )

    const prevSubscriptionData = await subscriptionModuleService
      .retrieveSubscription(
        subscription_id
      )

    const subscription = await subscriptionModuleService
      .recordNewSubscriptionOrder(
        subscription_id
      )

    return new StepResponse({
      subscription,
    }, {
      prev_data: prevSubscriptionData,
    })
  },
  async ({ 
    data,
  }, { container }) => {
    // TODO add compensation
  }
)

export default updateSubscriptionStep
```

This creates the `updateSubscriptionStep` that updates the subscriber using the `recordNewSubscriptionOrder` method of the Subscription Module’s main service. It returns the updated subscription.

Before updating the subscription, the step retrieves the old data and passes it to the compensation function to undo the changes on the subscription.

So, replace the `TODO` in the compensation function with the following:

```ts title="src/workflows/create-subscription-order/steps/update-subscription.ts"
if (!data) {
  return
}
const subscriptionModuleService: SubscriptionModuleService = 
  container.resolve(
    SUBSCRIPTION_MODULE
  )

await subscriptionModuleService.updateSubscriptions({
  id: data.prev_data.id,
  last_order_date: data.prev_data.last_order_date,
  next_order_date: data.prev_data.next_order_date,
})
```

This updates the subscription’s `last_order_date` and `next_order_date` properties to the values before the update.

### Create Workflow

Finally, create the file `src/workflows/create-subscription-order/index.ts` with the following content:

```ts title="src/workflows/create-subscription-order/index.ts" highlights={createSubscriptionOrderWorkflowHighlights} collapsibleLines="1-23" expandMoreLabel="Show Imports"
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { 
  useQueryGraphStep,
  createPaymentSessionsWorkflow,
  createRemoteLinkStep,
  capturePaymentStep,
} from "@medusajs/medusa/core-flows"
import { 
  SubscriptionData,
} from "../../modules/subscription/types"
import { 
  authorizePaymentSessionStep,
  createPaymentCollectionsStep,
} from "@medusajs/medusa/core-flows"
import createSubscriptionOrderStep, { 
  CreateSubscriptionOrderStepInput,
} from "./steps/create-subscription-order"
import updateSubscriptionStep from "./steps/update-subscription"
import { 
  getPaymentMethodStep, 
  GetPaymentMethodStepInput,
} from "./steps/get-payment-method"

type WorkflowInput = {
  subscription: SubscriptionData
}

const createSubscriptionOrderWorkflow = createWorkflow(
  "create-subscription-order",
  (input: WorkflowInput) => {
    const { data: subscriptions } = useQueryGraphStep({
      entity: "subscription",
      fields: [
        "*",
        "cart.*",
        "cart.items.*",
        "cart.items.tax_lines.*",
        "cart.items.adjustments.*",
        "cart.shipping_address.*",
        "cart.billing_address.*",
        "cart.shipping_methods.*",
        "cart.shipping_methods.tax_lines.*",
        "cart.shipping_methods.adjustments.*",
        "cart.payment_collection.*",
        "cart.payment_collection.payment_sessions.*",
        "cart.customer.*",
        "cart.customer.account_holder.*",
      ],
      filters: {
        id: input.subscription.id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const paymentCollectionData = transform({
      subscriptions,
    }, (data) => {
      const cart = data.subscriptions[0].cart
      return {
        currency_code: cart?.currency_code || "",
        amount: cart?.payment_collection?.amount || 0,
        metadata: cart?.payment_collection?.metadata || undefined,
      }
    })

    const payment_collection = createPaymentCollectionsStep([
      paymentCollectionData,
    ])[0]

    const defaultPaymentMethod = getPaymentMethodStep({
      customer: subscriptions[0].cart.customer,
    })

    const paymentSessionData = transform({
      payment_collection,
      subscriptions,
      defaultPaymentMethod,
    }, (data) => {
      return {
        payment_collection_id: data.payment_collection.id,
        provider_id: "pp_stripe_stripe",
        customer_id: data.subscriptions[0].cart?.customer?.id,
        data: {
          payment_method: data.defaultPaymentMethod.id,
          off_session: true,
          confirm: true,
          capture_method: "automatic",
        },
      }
    })

    const paymentSession = createPaymentSessionsWorkflow.runAsStep({
      input: paymentSessionData,
    })

    const payment = authorizePaymentSessionStep({
      id: paymentSession.id,
      context: paymentSession.context,
    })

    const { order, linkDefs } = createSubscriptionOrderStep({
      subscription: input.subscription,
      cart: carts[0],
      payment_collection,
    } as unknown as CreateSubscriptionOrderStepInput)

    createRemoteLinkStep(linkDefs)

    capturePaymentStep({
      payment_id: payment.id,
      amount: payment.amount,
    })

    updateSubscriptionStep({
      subscription_id: input.subscription.id,
    })

    return new WorkflowResponse({
      order,
    })
  }
)

export default createSubscriptionOrderWorkflow
```

The workflow runs the following steps:

1. `useQueryGraphStep` to retrieve the details of the cart linked to the subscription.
2. `createPaymentCollectionsStep` to create a payment collection using the same information in the cart.
3. `getPaymentMethodStep` to get the customer's saved payment method.
4. `createPaymentSessionsWorkflow` to create a payment session in the payment collection from the previous step. You prepare the data to create the payment session using [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK.
   - Since you're capturing the payment with Stripe, you must pass in the payment session's `data` object the following properties:
     - `payment_method`: the ID of the payment method saved in Stripe.
     - `off_session`: `true` to indicate that the payment is [off-session](https://docs.stripe.com/payments/payment-intents#future-usage).
     - `confirm`: `true` to confirm the payment.
     - `capture_method`: `automatic` to automatically capture the payment.
   - If you're using a payment provider other than Stripe, you'll need to adjust the `provider_id` value and the `data` object properties depending on what the provider expects.
5. `authorizePaymentSessionStep` to authorize the payment session created from the first step.
6. `createSubscriptionOrderStep` to create the new order for the subscription.
7. `createRemoteLinkStep` to create links returned by the previous step.
8. `capturePaymentStep` to capture the order’s payment.
9. `updateSubscriptionStep` to update the subscription’s `last_order_date` and `next_order_date`.

A workflow's constructor function has some constraints in implementation, which is why you need to use `transform` for data manipulation. Learn more about these constraints in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md).

In the next step, you’ll execute the workflow in a scheduled job.

### Further Reads

- [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md)

***

## Step 13: Create New Subscription Orders Scheduled Job

A scheduled job is an asynchronous function executed at a specified interval pattern. Use scheduled jobs to execute a task at a regular interval.

In this step, you’ll create a scheduled job that runs once a day. It finds all subscriptions whose `next_order_date` property is the current date and uses the workflow from the previous step to create an order for them.

Create the file `src/jobs/create-subscription-orders.ts` with the following content:

```ts title="src/jobs/create-subscription-orders.ts" highlights={createSubscriptionOrdersJob1Highlights} collapsibleLines="1-7" expandMoreLabel="Show Imports"
import { MedusaContainer } from "@medusajs/framework/types"
import SubscriptionModuleService from "../modules/subscription/service"
import { SUBSCRIPTION_MODULE } from "../modules/subscription"
import moment from "moment"
import createSubscriptionOrderWorkflow from "../workflows/create-subscription-order"
import { SubscriptionStatus } from "../modules/subscription/types"

export default async function createSubscriptionOrdersJob(
  container: MedusaContainer
) {
  const subscriptionModuleService: SubscriptionModuleService =
    container.resolve(SUBSCRIPTION_MODULE)
  const logger = container.resolve("logger")

  let page = 0
  const limit = 20
  let pagesCount = 0

  do {
    const beginningToday = moment(new Date()).set({
      second: 0,
      minute: 0,
      hour: 0,
    })
    .toDate()
    const endToday = moment(new Date()).set({
      second: 59,
      minute: 59,
      hour: 23,
    })
    .toDate()
  
    const [subscriptions, count] = await subscriptionModuleService
      .listAndCountSubscriptions({
        next_order_date: {
          $gte: beginningToday,
          $lte: endToday,
        },
        status: SubscriptionStatus.ACTIVE,
      }, {
        skip: page * limit,
        take: limit,
      })    

      // TODO create orders for subscriptions

    if (!pagesCount) {
      pagesCount = count / limit
    }
  
    page++
  } while (page < pagesCount - 1)
}

export const config = {
  name: "create-subscription-orders",
  schedule: "0 0 * * *", // Every day at midnight
}
```

This creates a scheduled job that runs once a day.

In the scheduled job, you retrieve subscriptions whose `next_order_date` is between the beginning and end of today, and whose `status` is `active`. You also support paginating the subscriptions in case there are more than `20` matching those filters.

To create orders for the subscriptions returned, replace the `TODO` with the following:

```ts title="src/jobs/create-subscription-orders.ts"
await Promise.all(
  subscriptions.map(async (subscription) => {
    try {
      const  { result } = await createSubscriptionOrderWorkflow(container)
        .run({
          input: {
            subscription,
          },
        })

      logger.info(`Created new order ${
        result.order.id
      } for subscription ${subscription.id}`)
    } catch (e) {
      logger.error(
        `Error creating a new order for subscription ${subscription.id}`,
        e
      )
    }
  })
)
```

This loops over the returned subscriptions and executes the `createSubscriptionOrderWorkflow` from the previous step to create the order.

### Further Reads

- [How to Create a Scheduled Job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md)

***

## Step 14: Expire Subscriptions Scheduled Job

In this step, you’ll create a scheduled job that finds subscriptions whose `expiration_date` is the current date and marks them as expired.

Before creating the scheduled job, add in `src/modules/subscription/service.ts` a new method:

```ts title="src/modules/subscription/service.ts"
class SubscriptionModuleService extends MedusaService({
  Subscription,
}) {
  // ...
  async expireSubscription(id: string | string[]): Promise<SubscriptionData[]> {
    const input = Array.isArray(id) ? id : [id]

    return await this.updateSubscriptions({
      selector: {
        id: input,
      },
      data: {
        next_order_date: null,
        status: SubscriptionStatus.EXPIRED,
      },
    })
  }
}
```

The `expireSubscription` updates the following properties of the specified subscriptions:

1. Set `next_order_date` to `null` as there are no more orders.
2. Set the `status` to `expired`.

Then, create the file `src/jobs/expire-subscription-orders.ts` with the following content:

```ts title="src/jobs/expire-subscription-orders.ts" highlights={expireSubscriptionOrdersJobHighlights} collapsibleLines="1-6" expandMoreLabel="Show Imports"
import { MedusaContainer } from "@medusajs/framework/types"
import SubscriptionModuleService from "../modules/subscription/service"
import { SUBSCRIPTION_MODULE } from "../modules/subscription"
import moment from "moment"
import { SubscriptionStatus } from "../modules/subscription/types"

export default async function expireSubscriptionOrdersJob(
  container: MedusaContainer
) {
  const subscriptionModuleService: SubscriptionModuleService =
    container.resolve(SUBSCRIPTION_MODULE)
  const logger = container.resolve("logger")

  let page = 0
  const limit = 20
  let pagesCount = 0

  do {
    const beginningToday = moment(new Date()).set({
      second: 0,
      minute: 0,
      hour: 0,
    })
    .toDate()
    const endToday = moment(new Date()).set({
      second: 59,
      minute: 59,
      hour: 23,
    })
    .toDate()
  
    const [subscriptions, count] = await subscriptionModuleService
      .listAndCountSubscriptions({
        expiration_date: {
          $gte: beginningToday,
          $lte: endToday,
        },
        status: SubscriptionStatus.ACTIVE,
      }, {
        skip: page * limit,
        take: limit,
      })    

    const subscriptionIds = subscriptions.map((subscription) => subscription.id)

    await subscriptionModuleService.expireSubscription(subscriptionIds)

    logger.log(`Expired ${subscriptionIds}.`)

    if (!pagesCount) {
      pagesCount = count / limit
    }
  
    page++
  } while (page < pagesCount - 1)
}

export const config = {
  name: "expire-subscriptions",
  schedule: "0 0 * * *", // Every day at midnight
}
```

This scheduled job runs once a day.

In the scheduled job, you find all subscriptions whose `expiration_date` is between the beginning and end of today and their status is `active`. Then, you use the `expireSubscription` method to expire those subscriptions.

You also implement pagination in case there are more than `20` expired subscriptions.

***

## Step 15: Add Customer API Routes

In this step, you’ll add two API routes for authenticated customers:

1. View their list of subscriptions.
2. Cancel a subscription.

### Create Subscriptions List API Route

Create the file `src/api/store/customers/me/subscriptions/route.ts` with the following content:

```ts title="src/api/store/customers/me/subscriptions/route.ts"
import { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: [customer] } = await query.graph({
    entity: "customer",
    fields: [
      "subscriptions.*",
    ],
    filters: {
      id: [req.auth_context.actor_id],
    },
  })

  res.json({
    subscriptions: customer.subscriptions,
  })
}
```

This adds an API route at `/store/customers/me/subscriptions`.

In the route handler, you retrieve the authenticated customer’s subscriptions using Query and return them in the response.

### Cancel Subscription API Route

Before creating this API route, add in `src/modules/subscription/service.ts` the following new method:

```ts title="src/modules/subscription/service.ts"
class SubscriptionModuleService extends MedusaService({
  Subscription,
}) {
  // ...
    
  async cancelSubscriptions(
    id: string | string[]): Promise<SubscriptionData[]> {
    const input = Array.isArray(id) ? id : [id]

    return await this.updateSubscriptions({
      selector: {
        id: input,
      },
      data: {
        next_order_date: null,
        status: SubscriptionStatus.CANCELED,
      },
    })
  }
}
```

The `cancelSubscriptions` method updates the specified subscribers to set their `next_order_date` to `null` and their status to `canceled`.

Then, create the file `src/api/store/customers/me/subscriptions/[id]/route.ts` with the following content:

```ts title="src/api/store/customers/me/subscriptions/[id]/route.ts"
import { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import SubscriptionModuleService from "../../../../../../modules/subscription/service"
import { 
  SUBSCRIPTION_MODULE,
} from "../../../../../../modules/subscription"

export const POST = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const subscriptionModuleService: SubscriptionModuleService =
    req.scope.resolve(SUBSCRIPTION_MODULE)

  const subscription = await subscriptionModuleService.cancelSubscriptions(
    req.params.id
  )

  res.json({
    subscription,
  })
}
```

This adds an API route at `/store/customers/me/subscriptions/[id]`. In the route handler, you use the `cancelSubscriptions` method added above to cancel the subscription whose ID is passed as a path parameter.

### Test it Out

To test out the above API routes, first, log in as a customer with the following request:

```bash
curl -X POST 'http://localhost:9000/auth/customer/emailpass' \
-H 'Content-Type: application/json' \
--data-raw '{
    "email": "customer@gmail.com",
    "password": "supersecret"
}'
```

Make sure to replace the `email` and `password` with the correct credentials.

If you don’t have a customer account, create one either using the Next.js Starter storefront or by following [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md).

Then, send a `GET` request to `/store/customers/me/subscriptions` to retrieve the customer’s subscriptions:

```bash
curl 'http://localhost:9000/store/customers/me/subscriptions' \
-H 'Authorization: Bearer {token}' \
-H 'x-publishable-api-key: {your_publishable_api_key}'
```

Where `{token}` is the token retrieved from the previous request.

To cancel a subscription, send a `POST` request to `/store/customers/me/subscriptions/[id]`, replacing the `[id]` with the ID of the subscription to cancel:

```bash
curl -X POST 'http://localhost:9000/store/customers/me/subscriptions/01J2VB8TVC14K29FREQ2DRS6NA' \
-H 'Authorization: Bearer {token}' \
-H 'x-publishable-api-key: {your_publishable_api_key}'
```

***

## Next Steps

The next steps of this example depend on your use case. This section provides some insight into implementing them.

### Use Existing Features

To manage the orders created for a subscription, or other functionalities, use Medusa’s existing [Admin API routes](https://docs.medusajs.com/api/admin).

### Link Subscriptions to Other Data Models

If your use case requires a subscription to have relations to other existing data models, you can create links to them, similar to [step four](#step-4-define-links).

For example, you can link a subscription to a promotion to offer a subscription-specific discount.

### Storefront Development

Medusa provides a Next.js Starter storefront that you can customize to your use case. You can also create a custom storefront. To learn how visit the [Storefront Development](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/index.html.md) section.


# Subscriptions Recipe

This recipe provides the general steps to build subscription-based purchase with Medusa.

## Overview

Subscription-based purchase allows customers to purchase products for a specified period, and the payment and fulfillment is processed within a regular interval in that period.

For example, a customer can purchase a book subscription box for a period of three months. Each month, the payment is captured for that order and, if the payment is successful, the fulfillment is processed.

Medusa's [Framework](https://docs.medusajs.com/docs/learn/fundamentals/framework/index.html.md) for customizations facilitates building subscription-based purchases. You can create a Subscription Module that implements data models for subscriptions, and link those data models to existing ones such as products and orders.

You can also expose custom features using API routes, and implement complex flows using workflows.

[How Goodchef built subscription-based purchases with Medusa](https://medusajs.com/blog/goodchef/).

***

## Save Subscription Details

Subscriptions have details related to the subscription interval, subscription period, and more.

To store the subscription details, you can create a data model in a new subscription module. The module's main service provides data management feature of the data model.

You can link the subscription data model to models of other modules, such as the Order Module's `Order` data model.

- [Create a Module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md): Learn how to create a module.
- [Create a Data Model](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md): Learn how to create a data model.

***

## Link Subscription to Existing Data Models

Define a module link that links a data model from your subscription module with a data model from another module.

For example, you can link the subscription data model to the Order Module's `Order` data model.

If you want to create subscriptions on the product level, you can link the subscription data model to the Product Module's `Product` data model.

[Define a Module Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md): Learn how to define a module link.

***

## Implement Subscription Approach

There are different ways to implement subscriptions in your Medusa application. This recipe covers two options.

### Option 1: Custom Subscription Logic

By implementing the subscription logic within your application, you have full control over the subscription logic. You'll also be independent of payment providers, providing customers with more than one payment provider.

Implementing the logic depends on your use case, but you'll mainly implement the following:

1. Create a workflow that completes a cart and creates a subscription for the order.
2. Create an API route that executes the workflow.
3. Create a scheduled job that checks daily for subscriptions that need renewal.
4. Create another scheduled job that checks daily for subscriptions that are expired.

- [Create a Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md): Learn how to create a workflow.
- [Create an API Route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md): Learn how to create an API route.

[Create a Scheduled Job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs#1-create-a-scheduled-job/index.html.md): Learn how to create a scheduled job.

### Option 2: Using Stripe Subscriptions

Stripe provides a [subscription payments feature](https://stripe.com/docs/billing/subscriptions/overview) that allows you to authorize payment on a subscription basis within Stripe. Stripe then handles checking for recurring payments and capturing payment at the specified interval.

This approach allows you to delegate the complications of implementing the subscription logic to Stripe, but doesn't support using other payment providers.

Although Medusa provides a [Stripe Payment Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md), it doesn't handle subscriptions. You can create a custom Stripe Subscription Module Provider instead.

[Create Payment Module Provider](https://docs.medusajs.com/references/payment/provider/index.html.md): Learn how to create a payment module provider.

***

## Customize Admin Dashboard

Based on your use case, you may need to customize the Medusa Admin to add new widgets or pages.

For example, you can create a page that lists all subscriptions or a widget that shows an order's subscription information.

The Medusa Admin is an extensible application within your Medusa application. You can customize it by:

- **Widgets**: Adding widgets to existing pages, such as the order page.
- **UI Routes**: Adding new pages to the Medusa Admin, such as a page to manage subscriptions.
- **Settings Pages**: Adding new pages to the Medusa Admin settings, such as a page to manage subscription settings.

- [Create Admin Widget](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md): Add widgets into existing admin pages.
- [Create Admin UI Routes](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md): Add new pages to your Medusa Admin.

[Create Admin Setting Page](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes#create-settings-page/index.html.md): Add new page to the Medusa Admin settings.

***

## Customize or Build Storefront

Medusa provides a Next.js Starter Storefront to use with your application. You can customize it for your subscription use case, such as allowing customers to manage their subscriptions.

Alternatively, you can build your own storefront using the Medusa APIs. This headless approach gives you the flexibility to build a custom storefront without limitations on which tech stack you use, or the design of the storefront.

- [Next.js Starter Storefront](https://docs.medusajs.com/nextjs-starter/index.html.md): Learn how to install and customize the Next.js Starter Storefront.
- [Storefront Development](https://docs.medusajs.com/storefront-development/index.html.md): Find guides to build your own storefront.


# Implement a Ticket Booking System with Medusa

In this tutorial, you'll learn how to implement a ticket booking system using Medusa.

This tutorial is divided into two parts: this part that covers the backend and admin customizations, and a [storefront part](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/ticket-booking/example/storefront/index.html.md) that covers the Next.js Starter Storefront customizations.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) that are available out-of-the-box.

Medusa's [Framework](https://docs.medusajs.com/docs/learn/fundamentals/framework/index.html.md) facilitates customizing Medusa's core features for your specific use case, such as ticket booking.

This tutorial provides an approach to implement a ticket booking system using Medusa. Depending on your specific requirements, you might need to adjust the implementation details or explore a different approach.

## Summary

By following this tutorial, you will learn how to:

- Install and set up Medusa with the Next.js Starter Storefront.
- Create data models for venues and tickets, and link them to Medusa's data models.
- Customize the Medusa Admin to manage venues and shows or events.
- Implement custom validation and flows for ticket booking.
- Generate QR codes for tickets and verify them at the venue.
- [Extend the Next.js Starter Storefront to allow booking tickets and choosing seats](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/ticket-booking/example/storefront/index.html.md): This part of the tutorial is covered separately.

![Diagram showing the architecture of the ticket booking system with Medusa](https://res.cloudinary.com/dza7lstvk/image/upload/v1757512301/Medusa%20Resources/ticket-booking-diagram_egbpye.jpg)

- [Full Code](https://github.com/medusajs/examples/tree/main/ticket-booking): Find the full code for this tutorial in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1757423563/OpenApi/Ticket_Booking_System_iqq1k6.yaml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.

Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Ticket Booking Module

In Medusa, you can build custom features in a [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a reusable package with the data models and functionality related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

In this step, you'll build a Ticket Booking Module that defines the data models and logic to manage venues and tickets. Later, you'll build commerce flows related to ticket booking around the module.

Refer to the [Modules documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to learn more.

### a. Create Module Directory

Create the directory `src/modules/ticket-booking` that will hold the Ticket Booking Module's code.

### b. Define Data Models

A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

Refer to the [Data Models documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md) to learn more.

You'll define data models to represent venues, tickets, and purchases. Later, you'll link these data models to Medusa's data models, such as products and orders.

#### Venue Model

The `Venue` data model represents a venue where shows or events take place.

To create the `Venue` data model, create the file `src/modules/ticket-booking/models/venue.ts` with the following content:

```ts title="src/modules/ticket-booking/models/venue.ts"
import { model } from "@medusajs/framework/utils"
import { VenueRow } from "./venue-row"

export const Venue = model.define("venue", {
  id: model.id().primaryKey(),
  name: model.text(),
  address: model.text().nullable(),
  rows: model.hasMany(() => VenueRow, {
    mappedBy: "venue",
  }),
})
.cascades({
  delete: ["rows"],
})

export default Venue
```

The `Venue` data model has the following properties:

- `id`: The primary key of the table.
- `name`: The name of the venue.
- `address`: The address of the venue.
- `rows`: A one-to-many relation with the `VenueRow` data model, which you'll create next.

Learn more about defining data model properties in the [Property Types documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md).

#### VenueRow Model

The `VenueRow` data model represents a row in a venue, each with a specific type and number of seats.

To create the `VenueRow` data model, create the file `src/modules/ticket-booking/models/venue-row.ts` with the following content:

```ts title="src/modules/ticket-booking/models/venue-row.ts"
import { model } from "@medusajs/framework/utils"
import { Venue } from "./venue"

export enum RowType {
  PREMIUM = "premium",
  BALCONY = "balcony",
  STANDARD = "standard",
  VIP = "vip"
}

export const VenueRow = model.define("venue_row", {
  id: model.id().primaryKey(),
  row_number: model.text(),
  row_type: model.enum(RowType),
  seat_count: model.number(),
  venue: model.belongsTo(() => Venue, {
    mappedBy: "rows",
  }),
})
.indexes([
  {
    on: ["venue_id", "row_number"],
    unique: true,
  },
])

export default VenueRow
```

The `VenueRow` data model has the following properties:

- `id`: The primary key of the table.
- `row_number`: The identifier of the row, such as "A" and "B", or "1" and "2".
- `row_type`: The type of the row, which can be "premium", "balcony", "standard", or "vip".
- `seat_count`: The number of seats in the row.
- `venue`: A many-to-one relation with the `Venue` data model, which represents the venue that the row belongs to.

You also add a unique index on the combination of `venue_id` and `row_number` to ensure that each row number is unique within a venue.

#### TicketProduct Model

The `TicketProduct` data model represents a product purchased as a ticket, such as a show or event. It will be linked to Medusa's `Product` data model.

To create the `TicketProduct` data model, create the file `src/modules/ticket-booking/models/ticket-product.ts` with the following content:

```ts title="src/modules/ticket-booking/models/ticket-product.ts"
import { model } from "@medusajs/framework/utils"
import { Venue } from "./venue"
import { TicketProductVariant } from "./ticket-product-variant"
import { TicketPurchase } from "./ticket-purchase"

export const TicketProduct = model.define("ticket_product", {
  id: model.id().primaryKey(),
  product_id: model.text().unique(),
  venue: model.belongsTo(() => Venue),
  dates: model.array(),
  variants: model.hasMany(() => TicketProductVariant, {
    mappedBy: "ticket_product",
  }),
  purchases: model.hasMany(() => TicketPurchase, {
    mappedBy: "ticket_product",
  }),
})
.indexes([
  {
    on: ["venue_id", "dates"],
  },
])

export default TicketProduct
```

The `TicketProduct` data model has the following properties:

- `id`: The primary key of the table.
- `product_id`: The ID of the linked product in Medusa's `Product` data model.
- `venue`: A many-to-one relation with the `Venue` data model, which represents the venue where the show takes place.
- `dates`: An array of dates when the show takes place.
- `variants`: A one-to-many relation with the `TicketProductVariant` data model, which you'll create next.
- `purchases`: A one-to-many relation with the `TicketPurchase` data model, which you'll create next.

You also add an index on the combination of `venue_id` and `dates` to optimize queries that filter by these fields.

Data relevant for ticket sales like price, inventory, etc., are all either included in the `Product` and `ProductVariant` data models or their linked records. So, you don't need to duplicate this information in the `TicketProduct` data model.

#### TicketProductVariant Model

The `TicketProductVariant` data model represents a variant of a ticket product, such as a specific row type. It will be linked to Medusa's `ProductVariant` data model.

To create the `TicketProductVariant` data model, create the file `src/modules/ticket-booking/models/ticket-product-variant.ts` with the following content:

```ts title="src/modules/ticket-booking/models/ticket-product-variant.ts"
import { model } from "@medusajs/framework/utils"
import { TicketProduct } from "./ticket-product"
import { RowType } from "./venue-row"
import { TicketPurchase } from "./ticket-purchase"

export const TicketProductVariant = model.define("ticket_product_variant", {
  id: model.id().primaryKey(),
  product_variant_id: model.text().unique(),
  ticket_product: model.belongsTo(() => TicketProduct, {
    mappedBy: "variants",
  }),
  row_type: model.enum(RowType),
  purchases: model.hasMany(() => TicketPurchase, {
    mappedBy: "ticket_variant",
  }),
})
.indexes([
  {
    on: ["ticket_product_id", "row_type"],
  },
])

export default TicketProductVariant
```

The `TicketProductVariant` data model has the following properties:

- `id`: The primary key of the table.
- `product_variant_id`: The ID of the linked product variant in Medusa's `ProductVariant` data model.
- `ticket_product`: A many-to-one relation with the `TicketProduct` data model, which represents the ticket product the variant belongs to.
- `row_type`: The type of the row associated with the variant, which can be "premium", "balcony", "standard", or "vip".
- `purchases`: A one-to-many relation with the `TicketPurchase` data model, which you'll create next.

#### TicketPurchase Model

The `TicketPurchase` data model represents the purchase of a seat for a specific `TicketProduct`. It will be linked to Medusa's `Order` data model.

To create the `TicketPurchase` data model, create the file `src/modules/ticket-booking/models/ticket-purchase.ts` with the following content:

```ts title="src/modules/ticket-booking/models/ticket-purchase.ts"
import { model } from "@medusajs/framework/utils"
import { TicketProduct } from "./ticket-product"
import { TicketProductVariant } from "./ticket-product-variant"
import { VenueRow } from "./venue-row"

export const TicketPurchase = model.define("ticket_purchase", {
  id: model.id().primaryKey(),
  order_id: model.text(),
  ticket_product: model.belongsTo(() => TicketProduct),
  ticket_variant: model.belongsTo(() => TicketProductVariant),
  venue_row: model.belongsTo(() => VenueRow),
  seat_number: model.text(),
  show_date: model.dateTime(),
  status: model.enum(["pending", "scanned"]).default("pending"),
})
.indexes([
  {
    on: ["order_id"],
  },
  {
    on: ["ticket_product_id", "venue_row_id", "seat_number", "show_date"],
    unique: true,
  },
])

export default TicketPurchase
```

The `TicketPurchase` data model has the following properties:

- `id`: The primary key of the table.
- `order_id`: The ID of the linked order in Medusa's `Order` data model.
- `ticket_product`: A many-to-one relation with the `TicketProduct` data model, which represents the ticket product purchased.
- `ticket_variant`: A many-to-one relation with the `TicketProductVariant` data model, which represents the variant (row type) of the ticket product purchased.
- `venue_row`: A many-to-one relation with the `VenueRow` data model, which represents the row of the seat purchased.
- `seat_number`: The number of the seat purchased.
- `show_date`: The date of the show for which the ticket was purchased.
- `status`: The status of the ticket purchase, which can be "pending" or "scanned". This is useful later when you add QR scanning functionality.

You also add two indexes:

- An index on the `order_id` field to optimize queries that filter by this field.
- A unique index on the combination of `ticket_product_id`, `venue_row_id`, `seat_number`, and `show_date` to ensure that a specific seat for a specific show date can only be purchased once.

### c. Create Module's Service

You can manage your module's data models in a service.

A service is a TypeScript class that the module exports. In the service's methods, you can connect to the database, allowing you to manage your data models, or connect to third-party services, which is useful if you're integrating with external services.

Refer to the [Module Service documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md) to learn more.

To create the Ticket Booking Module's service, create the file `src/modules/ticket-booking/service.ts` with the following content:

```ts title="src/modules/ticket-booking/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import Venue from "./models/venue"
import VenueRow from "./models/venue-row"
import TicketProduct from "./models/ticket-product"
import TicketProductVariant from "./models/ticket-product-variant"
import TicketPurchase from "./models/ticket-purchase"

export class TicketBookingModuleService extends MedusaService({
  Venue,
  VenueRow,
  TicketProduct,
  TicketProductVariant,
  TicketPurchase,
}) { }

export default TicketBookingModuleService
```

The `TicketBookingModuleService` extends `MedusaService`, which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods.

The `TicketBookingModuleService` class now has methods like `createTicketProducts` and `retrieveVenue`.

Find all methods generated by the `MedusaService` in [the Service Factory reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md).

### d. Export Module Definition

The final piece of a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/ticket-booking/index.ts` with the following content:

```ts title="src/modules/ticket-booking/index.ts"
import TicketBookingModuleService from "./service"
import { Module } from "@medusajs/framework/utils"

export const TICKET_BOOKING_MODULE = "ticketBooking"

export default Module(TICKET_BOOKING_MODULE, {
  service: TicketBookingModuleService,
})
```

You use the `Module` function to create the module's definition. It accepts two parameters:

1. The module's name, which is `ticketBooking`.
2. An object with a required property `service` indicating the module's service.

You also export the module's name as `TICKET_BOOKING_MODULE` so you can reference it later.

### e. Add Module to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/ticket-booking",
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

### f. Generate Migrations

Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript class that defines database changes made by a module.

Refer to the [Migrations documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md) to learn more.

Medusa's CLI tool can generate the migrations for you. To generate a migration for the Ticket Booking Module, run the following command in your Medusa application's directory:

```bash
npx medusa db:generate ticketBooking
```

The `db:generate` command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a `migrations` directory under `src/modules/ticket-booking` that holds the generated migration.

Then, to reflect these migrations on the database, run the following command:

```bash
npx medusa db:migrate
```

The tables for the Ticket Booking Module's data models are now created in the database.

***

## Step 3: Link Ticket Booking to Medusa Data Models

Since Medusa isolates modules to integrate them into your application without side effects, you can't directly create relationships between data models of different modules.

Instead, Medusa provides a mechanism to define links between data models and retrieve and manage linked records while maintaining module isolation.

Refer to the [Module Isolation documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md) to learn more.

In this step, you'll define a link between the data models in the Ticket Booking Module and Medusa's Commerce Modules.

### a. TicketProduct \<> Product Link

To define a link between the `TicketProduct` data model and Medusa's `Product` data model, create the file `src/links/ticket-product.ts` with the following content:

```ts title="src/links/ticket-product.ts"
import TicketingModule from "../modules/ticket-booking"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: TicketingModule.linkable.ticketProduct,
    deleteCascade: true,
  },
  ProductModule.linkable.product
)

```

You define a link using the `defineLink` function. It accepts two parameters:

1. An object indicating the first data model part of the link. A module has a special `linkable` property that contains link configurations for its data models. You pass the linkable configurations of the Ticket Booking Module's `TicketProduct` data model. You also set the `deleteCascade` property to `true`, indicating that a ticket product should be deleted if its linked product is deleted.
2. An object indicating the second data model part of the link. You pass the linkable configurations of the Product Module's `Product` data model.

In later steps, you'll learn how this link allows you to retrieve and manage ticket products and their related Medusa products.

Refer to the [Module Links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md) documentation to learn more about defining links.

### b. TicketProductVariant \<> ProductVariant Link

To define a link between the `TicketProductVariant` data model and Medusa's `ProductVariant` data model, create the file `src/links/ticket-product-variant.ts` with the following content:

```ts title="src/links/ticket-product-variant.ts"
import TicketingModule from "../modules/ticket-booking"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: TicketingModule.linkable.ticketProductVariant,
    deleteCascade: true,
  },
  ProductModule.linkable.productVariant
)
```

You define a link in a similar way as the previous link, but this time between the `TicketProductVariant` and `ProductVariant` data models.

### c. TicketPurchase \<> Order Link

Finally, to define a link between the `TicketPurchase` data model and Medusa's `Order` data model, create the file `src/links/ticket-purchase-order.ts` with the following content:

```ts title="src/links/ticket-purchase-order.ts"
import TicketingModule from "../modules/ticket-booking"
import OrderModule from "@medusajs/medusa/order"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: TicketingModule.linkable.ticketPurchase,
    deleteCascade: true,
    isList: true,
  },
  OrderModule.linkable.order
)

```

You define a link in a similar way as the previous links, but this time between the `TicketPurchase` and `Order` data models. You also set the `isList` property to `true` for the `TicketPurchase` data model, indicating that an order can have multiple ticket purchases.

### d. Sync Links to Database

After defining links, you need to sync them to the database. This creates the necessary tables to manage the links.

To sync the links to the database, run the migrations command again in the Medusa application's directory:

```bash
npx medusa db:migrate
```

This command will create the necessary tables to manage the links between the Ticket Booking Module's data models and Medusa's data models.

***

## Step 4: Create Venue

In this step, you'll implement the logic to create a venue.

When you build commerce features in Medusa that can be consumed by client applications, such as the Medusa Admin dashboard or storefront, you need to implement:

1. A [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) with steps that define the business logic of the feature.
2. An [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) that exposes the workflow's functionality to client applications.

In this step, you'll implement the workflow and API route for creating a venue.

### a. Create Venue Workflow

A workflow is a series of queries and actions, called steps, that complete a task. A workflow is similar to a function, but it allows you to track its executions' progress, define roll-back logic, and configure other advanced features.

Refer to the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) to learn more.

The workflow to create a venue will have the following steps:

- [createVenueStep](#createVenueStep): Creates a venue.
- [createVenueRowsStep](#createVenueRowsStep): Creates the venue's rows.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieves the created venue with its rows.

The `useQueryGraphStep` is available through Medusa's `@medusajs/medusa/core-flows` package. You'll implement other steps in the workflow.

#### createVenueStep

The `createVenueStep` creates a venue.

To create the step, create the file `src/workflows/steps/create-venue.ts` with the following content:

```ts title="src/workflows/steps/create-venue.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { TICKET_BOOKING_MODULE } from "../../modules/ticket-booking"

export type CreateVenueStepInput = {
  name: string
  address?: string
}

export const createVenueStep = createStep(
  "create-venue",
  async (input: CreateVenueStepInput, { container }) => {
    const ticketBookingModuleService = container.resolve(TICKET_BOOKING_MODULE)

    const venue = await ticketBookingModuleService.createVenues(input)

    return new StepResponse(venue, venue)
  },
  async (venue, { container }) => {
    if (!venue) {return}

    const ticketBookingModuleService = container.resolve(TICKET_BOOKING_MODULE)
    
    await ticketBookingModuleService.deleteVenues(venue.id)
  }
)
```

You create a step with the `createStep` function. It accepts three parameters:

1. The step's unique name.
2. An async function that receives two parameters:
   - The step's input, which is an object with the `name` and `address` properties of the venue to create.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.
3. An async compensation function that undoes the actions performed by the step function. This function is only executed if an error occurs during the workflow's execution.

In the step function, you resolve the Ticket Booking Module's service from the Medusa container using its `resolve` method, passing it the module's name as a parameter.

Then, you use the generated `createVenues` method of the Ticket Booking Module's service to create a venue with the provided input.

Finally, a step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters:

1. The step's output, which is the venue created.
2. Data to pass to the step's compensation function.

In the compensation function, you undo creating the venue by deleting it using the generated `deleteVenues` method of the Ticket Booking Module's service.

#### createVenueRowsStep

The `createVenueRowsStep` creates rows in a venue.

To create the step, create the file `src/workflows/steps/create-venue-rows.ts` with the following content:

```ts title="src/workflows/steps/create-venue-rows.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { TICKET_BOOKING_MODULE } from "../../modules/ticket-booking"
import { RowType } from "../../modules/ticket-booking/models/venue-row"

export type CreateVenueRowsStepInput = {
  rows: {
    venue_id: string
    row_number: string
    row_type: RowType
    seat_count: number
  }[]
}

export const createVenueRowsStep = createStep(
  "create-venue-rows",
  async (input: CreateVenueRowsStepInput, { container }) => {
    const ticketBookingModuleService = container.resolve(TICKET_BOOKING_MODULE)

    const venueRows = await ticketBookingModuleService.createVenueRows(
      input.rows
    )

    return new StepResponse(venueRows, venueRows)
  },
  async (venueRows, { container }) => {
    if (!venueRows) {return}

    const ticketBookingModuleService = container.resolve(TICKET_BOOKING_MODULE)
    
    await ticketBookingModuleService.deleteVenueRows(
      venueRows.map((row) => row.id)
    )
  }
)
```

This step receives the rows to create as an input.

In the step function, you create the rows and return them.

In the compensation function, you undo creating the rows by deleting them.

#### Create Venue Workflow

You can now create the workflow that uses the steps you implemented.

To create the workflow, create the file `src/workflows/create-venue.ts` with the following content:

```ts title="src/workflows/create-venue.ts"
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { createVenueStep } from "./steps/create-venue"
import { createVenueRowsStep } from "./steps/create-venue-rows"
import { RowType } from "../modules/ticket-booking/models/venue-row"
import { useQueryGraphStep } from "@medusajs/core-flows"

export type CreateVenueWorkflowInput = {
  name: string
  address?: string
  rows: Array<{
    row_number: string
    row_type: RowType
    seat_count: number
  }>
}

export const createVenueWorkflow = createWorkflow(
  "create-venue",
  (input: CreateVenueWorkflowInput) => {
    const venue = createVenueStep({
      name: input.name,
      address: input.address,
    })

    const venueRowsData = transform({
      venue,
      input,
    }, (data) => {
      return data.input.rows.map((row) => ({
        venue_id: data.venue.id,
        row_number: row.row_number,
        row_type: row.row_type,
        seat_count: row.seat_count,
      }))
    })

    createVenueRowsStep({
      rows: venueRowsData,
    })

    const { data: venues } = useQueryGraphStep({
      entity: "venue",
      fields: ["id", "name", "address", "rows.*"],
      filters: {
        id: venue.id,
      },
    })

    return new WorkflowResponse({
      venue: venues[0],
    })
  }
)
```

You create a workflow using the `createWorkflow` function. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function that holds the workflow's implementation. The function accepts an input object containing the details of the venue and its rows.

In the workflow, you:

1. Create a venue using the `createVenueStep`.
2. Prepare the data to create the venue's rows.
3. Create the venue's rows using the `createVenueRowsStep`.
4. Retrieve the created venue with its rows using the `useQueryGraphStep`.
   - This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) under the hood. It allows you to retrieve data across modules.

A workflow must return an instance of `WorkflowResponse` that accepts the data to return to the workflow's executor. You return the created venue with its rows.

`transform` allows you to access the values of data during execution. Learn more in the [Data Manipulation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) documentation.

### b. Create Venue API Route

Next, you'll create an API route that exposes the functionality of the `createVenueWorkflow` to client applications.

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

Refer to the [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) documentation to learn more about them.

Create the file `src/api/admin/venues/route.ts` with the following content:

```ts title="src/api/admin/venues/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { createVenueWorkflow } from "../../../workflows/create-venue"
import { RowType } from "../../../modules/ticket-booking/models/venue-row"
import { z } from "zod"

export const CreateVenueSchema = z.object({
  name: z.string(),
  address: z.string().optional(),
  rows: z.array(z.object({
    row_number: z.string(),
    row_type: z.nativeEnum(RowType),
    seat_count: z.number(),
  })),
})

type CreateVenueSchema = z.infer<typeof CreateVenueSchema>

export async function POST(
  req: MedusaRequest<CreateVenueSchema>,
  res: MedusaResponse
) {
  const { result } = await createVenueWorkflow(req.scope).run({
    input: req.validatedBody,
  })

  res.json(result)
}
```

You use [Zod](https://zod.dev/) to create the `CreateVenueSchema` that is used to validate request bodies sent to this API route.

Then, you export a `POST` route handler function, which will expose a `POST` API route at `/admin/venues`.

In the route handler, you execute the `createVenueWorkflow`, passing it the request body as an input.

Finally, you return the created venue with its rows in the response.

You'll test this API route later when you customize the Medusa Admin.

#### Add Validation Middleware for Create Venue API Route

To validate the body parameters of requests sent to the API route, you need to apply a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

To apply a middleware to a route, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares, 
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { CreateTicketProductSchema } from "./admin/ticket-products/route"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/admin/venues",
      methods: ["POST"],
      middlewares: [
        validateAndTransformBody(CreateVenueSchema),
      ],
    },
  ],
})
```

You apply Medusa's `validateAndTransformBody` middleware to `POST` requests sent to the `/admin/venues` route.

The middleware function accepts a Zod schema, which you created in the API route's file.

Refer to the [Middlewares](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) documentation to learn more.

***

## Step 5: List Venues API Route

In this step, you'll add an API route to retrieve a list of venues. You'll use this API route later to display a list of venues in the Medusa Admin.

To create the API route, add the following to the `src/api/admin/venues/route.ts` file:

```ts title="src/api/admin/venues/route.ts"
export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const query = req.scope.resolve("query")

  const { 
    data: venues,
    metadata,
  } = await query.graph({
    entity: "venue",
    ...req.queryConfig,
  })

  res.json({ 
    venues,
    count: metadata?.count,
    limit: metadata?.take,
    offset: metadata?.skip,
  })
}
```

You export a `GET` route handler function, which will expose a `GET` API route at `/admin/venues`.

In the route handler, you resolve Query from the Medusa container and use it to retrieve a list of venues.

Notice that you spread the `req.queryConfig` object into the `query.graph` method. This allows clients to pass query parameters for pagination and configure returned fields. You'll learn how to set these configurations in a bit.

Finally, you return the list of venues and pagination details in the response.

You'll test out this API route later when you customize the Medusa Admin.

### Add Validation Middleware for List Venues API Route

To validate the query parameters of requests sent to the API route, and to allow clients to configure pagination and returned fields, you need to apply a middleware.

To apply a middleware to the route, add the following imports at the top of the `src/api/middlewares.ts` file:

```ts title="src/api/middlewares.ts"
import { validateAndTransformQuery } from "@medusajs/framework/http"
import { createFindParams } from "@medusajs/medusa/api/utils/validators"
```

Then, add the following object to the `routes` array passed to `defineMiddlewares`:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/venues",
      methods: ["GET"],
      middlewares: [
        validateAndTransformQuery(createFindParams(), {
          isList: true,
          defaults: ["id", "name", "address", "rows.*"],
        }),
      ],
    },
  ],
})
```

You apply the `validateAndTransformQuery` middleware to `GET` requests sent to the `/admin/venues` route. This allows you to validate query parameters and set configurations for pagination and returned fields.

The middleware function accepts two parameters:

1. A Zod schema to validate the query parameters. You use Medusa's `createFindParams` utility function to create a schema that validates common query parameters like `limit`, `offset`, `fields`, and `order`.
2. Query configurations that you use in the API route using the `req.queryConfig` object. You set the following configurations:
   - `isList`: Set to `true` to indicate that the API route returns a list of records.
   - `defaults`: An array of fields to return by default if the client doesn't specify any fields in the request.

Refer to the [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#request-query-configurations/index.html.md) documentation to learn more about query request configurations.

***

## Step 6: Manage Venues in Medusa Admin

In this step, you'll customize the Medusa Admin to allow admin users to manage venues.

The Medusa Admin dashboard is customizable, allowing you to insert widgets into existing pages, or create new pages.

Refer to the [Admin Development](https://docs.medusajs.com/docs/learn/fundamentals/admin/index.html.md) documentation to learn more.

In this step, you'll create a new page or UI route in the Medusa Admin to view a list of venues and create new venues.

### a. Initialize JS SDK

To send requests to the Medusa server, you'll use the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md). It's already installed in your Medusa project, but you need to initialize it before using it in your customizations.

Create the file `src/admin/lib/sdk.ts` with the following content:

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

Learn more about the initialization options in the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) reference.

### b. Define Types

Next, you'll define types that you'll use in your admin customizations.

Create the file `src/admin/types.ts` with the following content:

```ts title="src/admin/types.ts"
export enum RowType {
  PREMIUM = "premium",
  BALCONY = "balcony",
  STANDARD = "standard",
  VIP = "vip"
}

export interface VenueRow {
  id: string
  row_number: string
  row_type: RowType
  seat_count: number
  venue_id: string
  created_at: string
  updated_at: string
}

export interface Venue {
  id: string
  name: string
  address?: string
  rows: VenueRow[]
  created_at: string
  updated_at: string
}

export interface CreateVenueRequest {
  name: string
  address?: string
  rows: {
    row_number: string
    row_type: RowType
    seat_count: number
  }[]
}

export interface VenuesResponse {
  venues: Venue[]
  count: number
  limit: number
  offset: number
}
```

You define types for the `RowType` enum, `VenueRow` and `Venue` data models, as well as types for API request and response bodies.

### c. Create Venues Page

You can now create a page that shows a list of venues in a table.

You create a page by creating a UI route. A UI route is a React component defined under the `src/admin/routes` directory in a `page.tsx` file. The path of the UI route is the file's path relative to `src/admin/routes`.

To create the venues page, create the file `src/admin/routes/venues/page.tsx` with the following content:

```tsx title="src/admin/routes/venues/page.tsx" collapsibleLines="1-15" expandButtonLabel="Show Imports"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Buildings } from "@medusajs/icons"
import { 
  createDataTableColumnHelper, 
  Container, 
  DataTable, 
  useDataTable, 
  Heading, 
  DataTablePaginationState,
} from "@medusajs/ui"
import { useQuery, useQueryClient } from "@tanstack/react-query"
import { useState, useMemo } from "react"
import { sdk } from "../../lib/sdk"
import { Venue, CreateVenueRequest } from "../../types"

const VenuesPage = () => {
  // TODO implement component
}

export const config = defineRouteConfig({
  label: "Venues",
  icon: Buildings,
})

export default VenuesPage
```

A UI route file must export:

- A React component as the default export. This component is rendered when the user navigates to the UI route.
- A route configuration object defined using the `defineRouteConfig` function. This object configures the UI route's label and icon in the sidebar.

In the `VenuesPage` component, you'll display the list of venues in a [Data Table](https://docs.medusajs.com/ui/components/data-table/index.html.md) component.

To define the columns of the data table, add the following before the `VenuesPage` component:

```tsx title="src/admin/routes/venues/page.tsx"
const columnHelper = createDataTableColumnHelper<Venue>()

const columns = [
  columnHelper.accessor("name", {
    header: "Name",
    cell: ({ row }) => (
      <div>
        <div className="txt-small-plus">{row.original.name}</div>
        {row.original.address && (
          <div className="txt-small text-gray-500">{row.original.address}</div>
        )}
      </div>
    ),
  }),
  columnHelper.accessor("rows", {
    header: "Total Capacity",
    cell: ({ row }) => {
      const totalCapacity = row.original.rows.reduce(
        (sum, rowItem) => sum + rowItem.seat_count,
        0
      )
      return <span className="txt-small-plus">{totalCapacity} seats</span>
    },
  }),
  columnHelper.accessor("address", {
    header: "Address",
    cell: ({ row }) => (
      <span>{row.original.address || "-"}</span>
    ),
  }),
]
```

You define three columns: `Name`, `Total Capacity`, and `Address`.

Next, to show the data table, replace the `VenuesPage` component with the following:

```tsx title="src/admin/routes/venues/page.tsx"
const VenuesPage = () => {
  const limit = 15
  const [pagination, setPagination] = useState<DataTablePaginationState>({
    pageSize: limit,
    pageIndex: 0,
  })

  const queryClient = useQueryClient()

  const offset = useMemo(() => {
    return pagination.pageIndex * limit
  }, [pagination])

  const { data, isLoading } = useQuery<{
    venues: Venue[]
    count: number
    limit: number
    offset: number
  }>({
    queryKey: ["venues", offset, limit],
    queryFn: () => sdk.client.fetch("/admin/venues", {
      query: {
        offset: pagination.pageIndex * pagination.pageSize,
        limit: pagination.pageSize,
        order: "-created_at",
      },
    }),
  })

  const table = useDataTable({
    columns,
    data: data?.venues || [],
    rowCount: data?.count || 0,
    isLoading,
    pagination: {
      state: pagination,
      onPaginationChange: setPagination,
    },
    getRowId: (row) => row.id,
  })

  return (
    <Container className="divide-y p-0">
      <DataTable instance={table}>
        <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
          <Heading>
            Venues
          </Heading>
        </DataTable.Toolbar>
        <DataTable.Table />
        <DataTable.Pagination />
      </DataTable>
    </Container>
  )
}
```

In the component, you use React Query's `useQuery` hook to fetch the list of venues from the `GET /admin/venues` API route you created earlier. You pass the `offset` and `limit` query parameters to paginate the results.

Then, you use the `useDataTable` hook from Medusa UI to create a data table instance with the fetched venues and the columns you defined earlier.

Finally, you render the data table.

### d. Create Venue Modal

Next, you'll add a component that shows a form in a modal to create a new venue. You'll open this modal when the user clicks a button on the venues page.

Before adding the modal, you'll add a component that visualizes the venue rows in a seat chart. You'll use this component in the modal to help admin users visualize the rows they're adding to the venue.

To create the seat chart component, create the file `src/admin/components/seat-chart.tsx` with the following content:

```tsx title="src/admin/components/seat-chart.tsx"
import React from "react"
import { Heading } from "@medusajs/ui"
import { RowType, VenueRow } from "../types"

interface ChartVenueRow extends Pick<VenueRow, "row_number" | "row_type" | "seat_count"> {}

interface SeatChartProps {
  rows: ChartVenueRow[]
  className?: string
}

const getRowTypeColor = (rowType: RowType): string => {
  switch (rowType) {
    case RowType.VIP:
      return "bg-purple-500"
    case RowType.PREMIUM:
      return "bg-orange-500"
    case RowType.BALCONY:
      return "bg-blue-500"
    case RowType.STANDARD:
      return "bg-gray-500"
    default:
      return "bg-gray-300"
  }
}

const getRowTypeLabel = (rowType: RowType): string => {
  switch (rowType) {
    case RowType.VIP:
      return "VIP"
    case RowType.PREMIUM:
      return "Premium"
    case RowType.BALCONY:
      return "Balcony"
    case RowType.STANDARD:
      return "Standard"
    default:
      return "Unknown"
  }
}

export const SeatChart = ({ rows, className = "" }: SeatChartProps) => {
  if (rows.length === 0) {
    return (
      <div className={`p-8 text-center text-gray-500 ${className}`}>
        <p>No rows added yet. Add rows to see the seat chart.</p>
      </div>
    )
  }

  // Sort rows by row_number for consistent display
  const sortedRows = [...rows].sort((a, b) => a.row_number.localeCompare(b.row_number))

  return (
    <div className={`space-y-4 ${className}`}>
      <div className="flex items-center justify-between">
        <Heading level="h3">Seat Chart Preview</Heading>
        <div className="flex items-center gap-4 txt-small">
          <div className="flex items-center gap-2">
            <div className="w-4 h-4 bg-purple-500 rounded"></div>
            <span>VIP</span>
          </div>
          <div className="flex items-center gap-2">
            <div className="w-4 h-4 bg-orange-500 rounded"></div>
            <span>Premium</span>
          </div>
          <div className="flex items-center gap-2">
            <div className="w-4 h-4 bg-blue-500 rounded"></div>
            <span>Balcony</span>
          </div>
          <div className="flex items-center gap-2">
            <div className="w-4 h-4 bg-gray-500 rounded"></div>
            <span>Standard</span>
          </div>
        </div>
      </div>

      <div className="border rounded-lg p-4 bg-gray-50">
        <div className="grid grid-cols-[auto_auto_1fr_auto] gap-4 items-center">
          {/* Header row */}
          <div className="txt-small-plus text-gray-700 text-center">Row</div>
          <div className="txt-small-plus text-gray-700 text-center">Type</div>
          <div className="txt-small-plus text-gray-700 text-center">Seats</div>
          <div className="txt-small-plus text-gray-700 text-center">Count</div>
          
          {/* Data rows */}
          {sortedRows.map((row) => (
            <React.Fragment key={row.row_number}>
              <div className="txt-small-plus text-gray-700 text-center">
                {row.row_number}
              </div>
              <div className="flex items-center justify-center gap-2">
                <div className={`w-4 h-4 rounded ${getRowTypeColor(row.row_type)}`}></div>
                <span className="txt-small text-ui-fg-subtle">
                  {getRowTypeLabel(row.row_type)}
                </span>
              </div>
              <div className="flex justify-center gap-1 flex-wrap">
                {Array.from({ length: row.seat_count }, (_, i) => (
                  <div
                    key={i}
                    className={`w-3 h-3 rounded-sm ${getRowTypeColor(row.row_type)} opacity-70`}
                  />
                ))}
              </div>
              <div className="txt-small text-gray-500 text-center">
                {row.seat_count}
              </div>
            </React.Fragment>
          ))}
        </div>
      </div>

      <div className="txt-small text-gray-500">
        Total capacity: {rows.reduce((sum, row) => sum + row.seat_count, 0)} seats
      </div>
    </div>
  )
}
```

This component accepts a list of venue rows and visualizes them in a seat chart.

Then, to create the component for the modal, create the file `src/admin/components/create-venue-modal.tsx` with the following content:

```tsx title="src/admin/components/create-venue-modal.tsx" collapsibleLines="1-14" expandButtonLabel="Show Imports"
import { useState } from "react"
import {
  FocusModal,
  Heading,
  Input,
  Select,
  Textarea,
  Label,
  Button,
  toast,
} from "@medusajs/ui"
import { CreateVenueRequest, RowType, VenueRow } from "../types"
import { SeatChart } from "./seat-chart"

interface NewVenueRow extends Pick<VenueRow, "row_number" | "row_type" | "seat_count"> {}

interface CreateVenueModalProps {
  open: boolean
  onOpenChange: (open: boolean) => void
  onSubmit: (data: CreateVenueRequest) => Promise<void>
}

export const CreateVenueModal = ({
  open,
  onOpenChange,
  onSubmit,
}: CreateVenueModalProps) => {
  const [name, setName] = useState("")
  const [address, setAddress] = useState("")
  const [rows, setRows] = useState<NewVenueRow[]>([])
  const [newRow, setNewRow] = useState({
    row_number: "",
    row_type: RowType.VIP,
    seat_count: 10,
  })
  const [isLoading, setIsLoading] = useState(false)

  // TODO add functions to manage venue rows
}
```

You create a `CreateVenueModal` component that accepts three props:

- `open`: A boolean indicating whether the modal is open or closed.
- `onOpenChange`: A function called when the modal's open state changes.
- `onSubmit`: A function called when the user submits the form to create a venue.

In the component, you define state variables to hold the venue's name, address, rows, a new row being added, and a loading state.

In the form, admin users should be able to add multiple rows to the venue. So, you'll add methods to manage rows, such as adding and removing rows.

Replace the `TODO` in the `CreateVenueModal` component with the following:

```tsx title="src/admin/components/create-venue-modal.tsx"
const addRow = () => {
  if (!newRow.row_number.trim()) {
    toast.error("Row number is required")
    return
  }

  if (rows.some((row) => row.row_number === newRow.row_number)) {
    toast.error("Row number already exists")
    return
  }

  if (newRow.seat_count <= 0) {
    toast.error("Seat count must be greater than 0")
    return
  }

  setRows([...rows, {
    row_number: newRow.row_number,
    row_type: newRow.row_type,
    seat_count: newRow.seat_count,
  }])
  setNewRow({
    row_number: "",
    row_type: RowType.VIP,
    seat_count: 10,
  })
}

const removeRow = (rowNumber: string) => {
  setRows(rows.filter((row) => row.row_number !== rowNumber))
}

const formatRowType = (rowType: RowType) => {
  switch (rowType) {
    case RowType.VIP:
      return "VIP"
    default:
      return rowType.charAt(0).toUpperCase() + rowType.slice(1).toLowerCase()
  }
}

// TODO handle form submission and modal close
```

You add the `addRow` function to add a new row to the venue, and the `removeRow` function to remove a row by its row number. You also add the `formatRowType` function to format the row type for display.

Next, you'll implement the logic to submit the form to create a venue and to close the modal and reset the form when the user closes it.

Replace the `TODO` in the `CreateVenueModal` component with the following:

```tsx title="src/admin/components/create-venue-modal.tsx"
const handleClose = () => {
  setName("")
  setAddress("")
  setRows([])
  setNewRow({
    row_number: "",
    row_type: RowType.VIP,
    seat_count: 10,
  })
  onOpenChange(false)
}

const handleSubmit = async (e: React.FormEvent) => {
  e.preventDefault()
  
  if (!name.trim()) {
    toast.error("Venue name is required")
    return
  }

  if (rows.length === 0) {
    toast.error("At least one row is required")
    return
  }

  setIsLoading(true)
  try {
    await onSubmit({
      name: name.trim(),
      address: address.trim() || undefined,
      rows: rows.map((row) => ({
        row_number: row.row_number,
        row_type: row.row_type,
        seat_count: row.seat_count,
      })),
    })
    handleClose()
  } catch (error: any) {
    toast.error(error.message)
  } finally {
    setIsLoading(false)
  }
}

// TODO render modal
```

You add the `handleClose` function to reset the form and call the `onOpenChange` prop to close the modal. You'll trigger this function when users close the modal or after successfully creating a venue.

You also add the `handleSubmit` function to validate the form data, call the `onSubmit` prop with the venue data, and handle loading and error states.

Finally, you'll render the modal with the form. Replace the `TODO` in the `CreateVenueModal` component with the following:

```tsx title="src/admin/components/create-venue-modal.tsx"
return (
  <FocusModal open={open} onOpenChange={handleClose}>
    <FocusModal.Content>
      <form onSubmit={handleSubmit} className="flex h-full flex-col overflow-hidden">
        <FocusModal.Header>
          <Heading level="h1">Create New Venue</Heading>
        </FocusModal.Header>
        <FocusModal.Body className="p-6 overflow-auto">
          <div className="max-w-[720px] mx-auto">
            <div className="space-y-4 w-fit mx-auto">
              <div>
                <Label htmlFor="name">Venue Name</Label>
                <Input
                  id="name"
                  value={name}
                  onChange={(e) => setName(e.target.value)}
                  placeholder="Enter venue name"
                />
              </div>

              <div>
                <Label htmlFor="address">
                  Address
                  <span className="text-ui-fg-muted txt-compact-small"> (Optional)</span>
                </Label>
                <Textarea
                  id="address"
                  value={address}
                  onChange={(e) => setAddress(e.target.value)}
                  placeholder="Enter venue address"
                  rows={3}
                />
              </div>

              <div className="border-t pt-4">
                <Heading level="h3" className="mb-2">Add Rows</Heading>
                
                <div className="space-y-3">
                  <div className="grid grid-cols-3 gap-3">
                    <div>
                      <Label htmlFor="row_number">Row Number</Label>
                      <Input
                        id="row_number"
                        value={newRow.row_number}
                        onChange={(e) => setNewRow({ ...newRow, row_number: e.target.value })}
                        placeholder="A, B, 1, 2..."
                      />
                    </div>
                    
                    <div>
                      <Label htmlFor="row_type">Row Type</Label>
                      <Select
                        value={newRow.row_type}
                        onValueChange={(value) => setNewRow({ ...newRow, row_type: value as RowType })}
                      >
                        <Select.Trigger>
                          <Select.Value />
                        </Select.Trigger>
                        <Select.Content>
                          <Select.Item value={RowType.VIP}>VIP</Select.Item>
                          <Select.Item value={RowType.PREMIUM}>Premium</Select.Item>
                          <Select.Item value={RowType.BALCONY}>Balcony</Select.Item>
                          <Select.Item value={RowType.STANDARD}>Standard</Select.Item>
                        </Select.Content>
                      </Select>
                    </div>
                    
                    <div>
                      <Label htmlFor="seat_count">Seat Count</Label>
                      <Input
                        id="seat_count"
                        type="number"
                        min="1"
                        value={newRow.seat_count}
                        onChange={(e) => setNewRow({ ...newRow, seat_count: parseInt(e.target.value) || 0 })}
                      />
                    </div>
                  </div>
                  
                  <Button
                    type="button"
                    variant="secondary"
                    onClick={addRow}
                    disabled={!newRow.row_number.trim()}
                  >
                    Add Row
                  </Button>
                </div>

                {rows.length > 0 && (
                  <div className="mt-4">
                    <h4 className="txt-small-plus mb-2">Added Rows</h4>
                    <div className="space-y-2">
                      {rows.map((row) => (
                        <div key={row.row_number} className="flex items-center justify-between p-2 bg-ui-bg-subtle rounded">
                          <span className="txt-small">
                            Row {row.row_number} - {formatRowType(row.row_type)} ({row.seat_count} seats)
                          </span>
                          <Button
                            type="button"
                            variant="danger"
                            size="small"
                            onClick={() => removeRow(row.row_number)}
                          >
                            Remove
                          </Button>
                        </div>
                      ))}
                    </div>
                  </div>
                )}
              </div>
            </div>

            <hr className="my-10" />

            <div>
              <SeatChart rows={rows} />
            </div>
          </div>
        </FocusModal.Body>
        <FocusModal.Footer>

          <Button
            type="submit"
            variant="primary"
            isLoading={isLoading}
            disabled={!name.trim() || rows.length === 0}
          >
            Create Venue
          </Button>
        </FocusModal.Footer>
      </form>
    </FocusModal.Content>
  </FocusModal>
)
```

You use Medusa UI's `FocusModal` component to render the modal. Inside the modal, you render a form with input fields for the venue's name and address, and fields to add rows.

You also render the `SeatChart` component to visualize the added rows.

Now that the `CreateVenueModal` component is ready, you'll use it in the `VenuesPage` component.

In `src/admin/routes/venues/page.tsx`, add the following imports at the top of the file:

```tsx title="src/admin/routes/venues/page.tsx"
import { 
  Button,
} from "@medusajs/ui"
import { CreateVenueModal } from "../../components/create-venue-modal"
```

Then, in the `VenuesPage` component, add the following state variable to manage the modal's open state:

```tsx title="src/admin/routes/venues/page.tsx"
const VenuesPage = () => {
  const [isModalOpen, setIsModalOpen] = useState(false)
  // ...
}
```

Next, add the following functions before the `return` statement to handle opening and closing the modal, and to handle creating a venue:

```tsx title="src/admin/routes/venues/page.tsx"
const VenuesPage = () => {
  // ...
  const handleCloseModal = () => {
    setIsModalOpen(false)
  }

  const handleCreateVenue = async (data: CreateVenueRequest) => {
    try {
      await sdk.client.fetch("/admin/venues", {
        method: "POST",
        body: data,
      })
      queryClient.invalidateQueries({ queryKey: ["venues"] })
      handleCloseModal()
    } catch (error: any) {
      throw new Error(`Failed to create venue: ${error.message}`)
    }
  }
  // ...
}
```

You add the `handleCloseModal` function to close the modal by setting the `isModalOpen` state to `false`.

You also add the `handleCreateVenue` function to send a `POST` request to the `/admin/venues` API route you created earlier.

Then, to trigger opening the modal, add the following button as the last child of `<DataTable.Toolbar>` in the `return` statement:

```tsx title="src/admin/routes/venues/page.tsx"
return (
  <Container className="divide-y p-0">
    {/* ... */}
      <Button
        variant="secondary"
        onClick={() => setIsModalOpen(true)}
      >
        Create Venue
      </Button>
    {/* ... */}
  </Container>
)
```

Finally, render the `CreateVenueModal` component as the last child of the `<Container>` component in the `return` statement:

```tsx title="src/admin/routes/venues/page.tsx"
return (
  <Container className="divide-y p-0">
    {/* ... */}
    <CreateVenueModal
      open={isModalOpen}
      onOpenChange={handleCloseModal}
      onSubmit={handleCreateVenue}
    />
  </Container>
)
```

You render the `CreateVenueModal` component, passing it the `isModalOpen` state, and the `handleCloseModal` and `handleCreateVenue` functions as props.

### Test the Venues Page

You can now test the venues page in the Medusa Admin.

Run the following command to start the Medusa server:

```bash npm2yarn
npm run dev
```

Then, open `http://localhost:9000/app` in your browser to access the Medusa Admin. Log in with the admin user you created earlier.

You should see a new "Venues" item in the sidebar. Click on it to navigate to the venues page. You'll see an empty table with a "Create Venue" button.

![Venues Page in the Medusa Admin with an empty table](https://res.cloudinary.com/dza7lstvk/image/upload/v1757430925/Medusa%20Resources/CleanShot_2025-09-09_at_18.15.02_2x_o0pf16.png)

To create a venue:

1. Click the "Create Venue" button to open the modal.
2. In the modal, enter a name for the venue, and optionally an address.
3. Add rows and visualize them in the seat chart.
4. Once you're done, click the "Create Venue" button to create the venue.

![Create Venue Modal in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1757431196/Medusa%20Resources/CleanShot_2025-09-09_at_18.19.45_2x_qyov67.png)

After creating the venue, you can see it listed in the table.

***

## Step 7: Create Ticket Product

In this step, you'll add functionality to create a ticket product for a show. You'll create a workflow and an API route to handle ticket product creation.

### a. Create Create Ticket Product Workflow

The workflow that creates a ticket product will also create the Medusa product and its variants. It will create a variant for each show date and row type. For example, if a show has two dates and three row types, the workflow will create six variants for the Medusa product.

The workflow will also create inventory items for each variant, ensuring that customers can't purchase more tickets than the venue's capacity.

{/* TODO add diagram */}

The workflow will have the following steps:

- [validateVenueAvailabilityStep](#validateVenueAvailabilityStep): Validates that the selected venue is available for the show date and time.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve default store configuration that are useful for creating the Medusa product.
- [createInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryItemsWorkflow/index.html.md): Creates inventory items for the Medusa product.
- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md): Creates the Medusa product and its variants.
- [createTicketProductsStep](#createTicketProductsStep): Creates the ticket product.
- [createTicketProductVariantsStep](#createTicketProductVariantsStep): Creates the ticket product variants.
- [createRemoteLinkStep](#createRemoteLinkStep): Create links between the ticket product and variants and the Medusa product and variants.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the created ticket product with its relations.

You only need to implement the `validateVenueAvailabilityStep`, `createTicketProductsStep`, and `createTicketProductVariantsStep` steps. The other steps are provided by Medusa.

#### validateVenueAvailabilityStep

The `validateVenueAvailabilityStep` validates that the selected venue is available for the show's date and time.

To create the step, create the file `src/workflows/steps/validate-venue-availability.ts` with the following content:

```ts title="src/workflows/steps/validate-venue-availability.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { TICKET_BOOKING_MODULE } from "../../modules/ticket-booking"
import { MedusaError } from "@medusajs/framework/utils"

export type ValidateVenueAvailabilityStepInput = {
  venue_id: string
  dates: string[]
}

export const validateVenueAvailabilityStep = createStep(
  "validate-venue-availability",
  async (input: ValidateVenueAvailabilityStepInput, { container }) => {
    const ticketBookingModuleService = container.resolve(TICKET_BOOKING_MODULE)

    // Get all existing ticket products for this venue
    const existingTicketProducts = await ticketBookingModuleService
      .listTicketProducts({
        venue_id: input.venue_id,
      })

    const hasConflict = existingTicketProducts.some((ticketProduct) => 
      ticketProduct.dates.some((date) => input.dates.includes(date))
    )

    if (hasConflict) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA, 
        `Venue has conflicting shows on dates: ${input.dates.join(", ")}`
      )
    }

    return new StepResponse({ valid: true })
  }
)
```

In the step, you retrieve all existing ticket products for the selected venue and throw an error if any of the existing ticket products has a date that conflicts with the new show's dates.

#### createTicketProductsStep

The `createTicketProductsStep` creates ticket products.

To create the step, create the file `src/workflows/steps/create-ticket-products.ts` with the following content:

```ts title="src/workflows/steps/create-ticket-products.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { TICKET_BOOKING_MODULE } from "../../modules/ticket-booking"

export type CreateTicketProductsStepInput = {
  ticket_products: {
    product_id: string
    venue_id: string
    dates: string[]
  }[]
}

export const createTicketProductsStep = createStep(
  "create-ticket-products",
  async (input: CreateTicketProductsStepInput, { container }) => {
    const ticketBookingModuleService = container.resolve(TICKET_BOOKING_MODULE)

    // Create the main ticket product
    const ticketProducts = await ticketBookingModuleService
      .createTicketProducts(
        input.ticket_products
      )

    return new StepResponse(
      { 
        ticket_products: ticketProducts,
      },
      { 
        ticket_products: ticketProducts,
      }
    )
  },
  async (compensationData, { container }) => {
    if (!compensationData?.ticket_products) {return}

    const ticketBookingModuleService = container.resolve(TICKET_BOOKING_MODULE)
    
    // Delete the ticket product
    await ticketBookingModuleService.deleteTicketProducts(
      compensationData.ticket_products.map((tp) => tp.id)
    )
  }
)
```

This step receives an array of ticket products to create. It creates the ticket products and returns them.

In the compensation function, you delete the created ticket products if an error occurs in the workflow.

#### createTicketProductVariantsStep

The `createTicketProductVariantsStep` creates ticket product variants.

To create the step, create the file `src/workflows/steps/create-ticket-product-variants.ts` with the following content:

```ts title="src/workflows/steps/create-ticket-product-variants.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { TICKET_BOOKING_MODULE } from "../../modules/ticket-booking"
import { RowType } from "../../modules/ticket-booking/models/venue-row"

export type CreateTicketProductVariantsStepInput = {
  variants: {
    ticket_product_id: string
    product_variant_id: string
    row_type: RowType
  }[]
}

export const createTicketProductVariantsStep = createStep(
  "create-ticket-product-variants",
  async (input: CreateTicketProductVariantsStepInput, { container }) => {
    const ticketBookingModuleService = container.resolve(TICKET_BOOKING_MODULE)

    // Create ticket product variants for each Medusa variant
    const ticketVariants = await ticketBookingModuleService
      .createTicketProductVariants(
        input.variants
      )

    return new StepResponse(
      {
        ticket_product_variants: ticketVariants,
      },
      {
        ticket_product_variants: ticketVariants,
      }
    )
  },
  async (compensationData, { container }) => {
    if (!compensationData?.ticket_product_variants) {return}

    const ticketBookingModuleService = container.resolve(TICKET_BOOKING_MODULE)
    
    await ticketBookingModuleService.deleteTicketProductVariants(
      compensationData.ticket_product_variants.map((v) => v.id)
    )
  }
)
```

This step receives an array of ticket product variants to create. It creates the ticket product variants and returns them.

In the compensation function, you delete the created ticket product variants if an error occurs in the workflow.

#### Create Ticket Product Workflow

You can now create the workflow that creates a ticket product.

To create the workflow, create the file `src/workflows/create-ticket-product.ts` with the following content:

```ts title="src/workflows/create-ticket-product.ts" collapsibleLines="1-10" expandButtonLabel="Show Imports"
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { validateVenueAvailabilityStep } from "./steps/validate-venue-availability"
import { createTicketProductsStep } from "./steps/create-ticket-products"
import { useQueryGraphStep, createProductsWorkflow, createRemoteLinkStep, createInventoryItemsWorkflow } from "@medusajs/medusa/core-flows"
import { CreateProductWorkflowInputDTO, CreateMoneyAmountDTO } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
import { TICKET_BOOKING_MODULE } from "../modules/ticket-booking"
import { RowType } from "../modules/ticket-booking/models/venue-row"
import { createTicketProductVariantsStep } from "./steps/create-ticket-product-variants"

export type CreateTicketProductWorkflowInput = {
  name: string
  venue_id: string
  dates: string[]
  variants: Array<{
    row_type: RowType
    seat_count: number
    prices: CreateMoneyAmountDTO[]
  }>
}

export const createTicketProductWorkflow = createWorkflow(
  "create-ticket-product",
  (input: CreateTicketProductWorkflowInput) => {
    validateVenueAvailabilityStep({
      venue_id: input.venue_id,
      dates: input.dates,
    })

    const { data: stores } = useQueryGraphStep({
      entity: "store",
      fields: ["id", "default_location_id", "default_sales_channel_id"],
    })

    // TODO create inventory items for each variant
  }
)
```

You create the `createTicketProductWorkflow` workflow that accepts the details of the ticket product and its variants.

In the workflow, you validate the venue's availability using the `validateVenueAvailabilityStep`.

Then, you retrieve the default store configuration using the `useQueryGraphStep`. These configurations are useful when creating the Medusa inventory items and product.

Make sure you have [default location and sales channel set up in your Medusa store](https://docs.medusajs.com/user-guide/settings/store/index.html.md).

Next, you'll create inventory items for each variant before creating the Medusa product. Replace the `TODO` with the following:

```ts title="src/workflows/create-ticket-product.ts"
const inventoryItemsData = transform({
  input,
  stores,
}, (data) => {
  const inventoryItems: any[] = []
  
  for (const date of data.input.dates) {
    for (const variant of data.input.variants) {
      inventoryItems.push({
        sku: `${data.input.name}-${date}-${variant.row_type}`,
        title: `${data.input.name} - ${date} - ${variant.row_type}`,
        description: `Ticket for ${data.input.name} on ${date} in ${variant.row_type} seating`,
        location_levels: [{
          location_id: data.stores[0].default_location_id,
          stocked_quantity: variant.seat_count,
        }],
        requires_shipping: false,
      })
    }
  }
  
  return inventoryItems
})

const inventoryItems = createInventoryItemsWorkflow.runAsStep({
  input: {
    items: inventoryItemsData,
  },
})

// TODO create the Medusa product
```

You prepare the inventory items to be created using the `transform` function. You create an inventory item for each combination of show date and variant.

Notice that for each inventory item, you set the `stocked_quantity` to the `seat_count` of the variant, ensuring that the inventory reflects the venue's seating capacity. You also set `requires_shipping` to `false`, allowing you to skip the shipping step during checkout.

Then, you create the inventory items using the `createInventoryItemsWorkflow`.

Next, you'll create the Medusa product and variants. Replace the `TODO` with the following:

```ts title="src/workflows/create-ticket-product.ts"
const productData = transform({
  input,
  inventoryItems,
  stores,
}, (data) => {
  const rowTypes = [...new Set(
    data.input.variants.map((variant: any) => variant.row_type)
  )]
  
  const product: CreateProductWorkflowInputDTO = {
    title: data.input.name,
    status: "published",
    options: [
      {
        title: "Date",
        values: data.input.dates,
      },
      {
        title: "Row Type", 
        values: rowTypes,
      },
    ],
    variants: [] as any[],
  }

  if (data.stores[0].default_sales_channel_id) {
    product.sales_channels = [
      {
        id: data.stores[0].default_sales_channel_id,
      },
    ]
  }

  // Create variants for each date and row type combination
  let inventoryIndex = 0
  for (const date of data.input.dates) {
    for (const variant of data.input.variants) {
      product.variants!.push({
        title: `${data.input.name} - ${date} - ${variant.row_type}`,
        options: {
          Date: date,
          "Row Type": variant.row_type,
        },
        manage_inventory: true,
        inventory_items: [{
          inventory_item_id: data.inventoryItems[inventoryIndex].id,
        }],
        prices: variant.prices,
      })
      inventoryIndex++
    }
  }

  return [product]
})

const medusaProduct = createProductsWorkflow.runAsStep({
  input: {
    products: productData,
  },
})

// TODO create the ticket product and variants
```

You prepare the Medusa product data using the `transform` function. You create options for "Date" and "Row Type" and create a variant for each combination of show date and row type.

You associate each variant with the corresponding inventory item created earlier, ensuring that the inventory is correctly linked to the product variants.

Then, you create the Medusa product using the `createProductsWorkflow`.

Next, you'll create the ticket product and its variants. Replace the `TODO` with the following:

```ts title="src/workflows/create-ticket-product.ts"
const ticketProductData = transform({
  medusaProduct,
  input,
}, (data) => {
  return {
    ticket_products: data.medusaProduct.map((product: any) => ({
      product_id: product.id,
      venue_id: data.input.venue_id,
      dates: data.input.dates,
    })),
  }
})

const { ticket_products } = createTicketProductsStep(
  ticketProductData
)

const ticketVariantsData = transform({
  medusaProduct,
  ticket_products,
  input,
}, (data) => {
  return {
    variants: data.medusaProduct[0].variants.map((variant: any) => {
      const rowType = variant.options.find(
        (opt: any) => opt.option?.title === "Row Type"
      )?.value
      return {
        ticket_product_id: data.ticket_products[0].id,
        product_variant_id: variant.id,
        row_type: rowType,
      }
    }),
  }
})

const { ticket_product_variants } = createTicketProductVariantsStep(
  ticketVariantsData
)

// TODO create links and retrieve the created ticket product
```

You prepare the ticket product data using the `transform` function, then create the ticket product using the `createTicketProductsStep`.

You also prepare the ticket product variants data, then create the ticket product variants using the `createTicketProductVariantsStep`.

Finally, you'll create links between the ticket product and variants and the Medusa product and variants, and retrieve the created ticket product with its relations. Replace the `TODO` with the following:

```ts title="src/workflows/create-ticket-product.ts"
const linksData = transform({
  medusaProduct,
  ticket_products,
  ticket_product_variants,
}, (data) => {
  // Create links between ticket product and Medusa product
  const productLinks = [{
    [TICKET_BOOKING_MODULE]: {
      ticket_product_id: data.ticket_products[0].id,
    },
    [Modules.PRODUCT]: {
      product_id: data.medusaProduct[0].id,
    },
  }]

  // Create links between ticket variants and Medusa variants
  const variantLinks = data.ticket_product_variants.map((variant) => ({
    [TICKET_BOOKING_MODULE]: {
      ticket_product_variant_id: variant.id,
    },
    [Modules.PRODUCT]: {
      product_variant_id: variant.product_variant_id,
    },
  }))

  return [...productLinks, ...variantLinks]
})

createRemoteLinkStep(linksData)

const { data: finalTicketProduct } = useQueryGraphStep({
  entity: "ticket_product",
  fields: [
    "id",
    "product_id",
    "venue_id",
    "dates",
    "venue.*",
    "product.*",
    "variants.*",
  ],
  filters: {
    id: ticket_products[0].id,
  },
}).config({ name: "retrieve-ticket-product" })

return new WorkflowResponse({
  ticket_product: finalTicketProduct[0],
})
```

You create links between the ticket product and the Medusa product, and between the ticket product variants and the Medusa product variants using the `createRemoteLinkStep`.

Then, you retrieve the created ticket product with its relations using the `useQueryGraphStep`.

Finally, you return the created ticket product in the workflow response.

### b. Create Ticket Product API Route

Next, you'll create an API route that allows admin users to create a ticket product.

To create the API route, create the file `src/api/admin/ticket-products/route.ts` with the following content:

```ts title="src/api/admin/ticket-products/route.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import {
  createTicketProductWorkflow,
} from "../../../workflows/create-ticket-product"
import { RowType } from "../../../modules/ticket-booking/models/venue-row"
import { z } from "zod"

export const CreateTicketProductSchema = z.object({
  name: z.string().min(1, "Name is required"),
  venue_id: z.string().min(1, "Venue ID is required"),
  dates: z.array(z.string()).min(1, "At least one date is required"),
  variants: z.array(z.object({
    row_type: z.nativeEnum(RowType),
    seat_count: z.number().min(1, "Seat count must be at least 1"),
    prices: z.array(z.object({
      currency_code: z.string().min(1, "Currency code is required"),
      amount: z.number().min(0, "Amount must be non-negative"),
      min_quantity: z.number().optional(),
      max_quantity: z.number().optional(),
    })).min(1, "At least one price is required"),
  })).min(1, "At least one variant is required"),
})

type CreateTicketProductSchema = z.infer<typeof CreateTicketProductSchema>

export async function POST(
  req: MedusaRequest<CreateTicketProductSchema>,
  res: MedusaResponse
) {
  const { result } = await createTicketProductWorkflow(req.scope).run({
    input: req.validatedBody,
  })

  res.json(result)
}
```

You define the validation schema `CreateTicketProductSchema` that will be used to validate the request body.

Then, you export a `POST` route handler function, which will expose a `POST` API route at `/admin/ticket-products`.

In the route handler, you execute the `createTicketProductWorkflow` and return the created ticket product in the response.

You'll test the API route when you customize the Medusa Admin later.

### c. Create Ticket Product Validation Middleware

To validate the request body against the schema you defined for creating ticket products, you'll apply a validation middleware to the API route.

In `src/api/middlewares.ts`, add the following import at the top of the file:

```ts title="src/api/middlewares.ts"
import { CreateTicketProductSchema } from "./admin/ticket-products/route"
```

Then, add a new object to the `routes` array passed to the `defineMiddlewares` function:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/ticket-products",
      methods: ["POST"],
      middlewares: [
        validateAndTransformBody(CreateTicketProductSchema),
      ],
    },
  ],
})
```

You apply the `validateAndTransformBody` middleware to the `POST /admin/ticket-products` route, passing it the `CreateTicketProductSchema` for validation.

***

## Step 8: List Ticket Products API Route

In this step, you'll add an API route to list ticket products. This will be useful when you customize the Medusa Admin to display ticket products.

To create the API route, add the following to the `src/api/admin/ticket-products/route.ts` file:

```ts title="src/api/admin/ticket-products/route.ts"
export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const query = req.scope.resolve("query")

  const {
    data: ticketProducts,
    metadata,
  } = await query.graph({
    entity: "ticket_product",
    ...req.queryConfig,
  })

  res.json({
    ticket_products: ticketProducts,
    count: metadata?.count,
    limit: metadata?.take,
    offset: metadata?.skip,
  })
}
```

You export a `GET` route handler function, which will expose a `GET` API route at `/admin/ticket-products`.

In the route handler, you use Query to retrieve ticket products. You pass the `req.queryConfig` object to support pagination and field selection based on query configurations and parameters.

Finally, you return the ticket products along with pagination metadata in the response.

You'll test the API route when you customize the Medusa Admin later.

### Add Validation Middleware for List Ticket Products API Route

To validate the query parameters of requests sent to the API route, and to allow clients to configure pagination and returned fields, you need to apply a query validation middleware.

To apply a middleware to the route, in `src/api/middlewares.ts`, add the following object to the `routes` array passed to the `defineMiddlewares` function:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/ticket-products",
      methods: ["GET"],
      middlewares: [
        validateAndTransformQuery(createFindParams(), {
          isList: true,
          defaults: [
            "id", 
            "product_id", 
            "venue_id", 
            "dates", 
            "venue.*", 
            "variants.*", 
            "product.*",
          ],
        }),
      ],
    },
  ],
})
```

You apply the `validateAndTransformQuery` middleware to `GET` requests sent to the `/admin/ticket-products` route. You pass it the `createFindParams` function to allow passing pagination and field selection parameters.

You also define the default fields of a ticket product to be returned in the response.

***

## Step 9: Manage Ticket Products in Medusa Admin

In this step, you'll customize the Medusa Admin to add a new page that shows a list of ticket products and allows creating new ticket products.

### a. Define Ticket Product Type

Before you customize the Medusa Admin, you'll define a type for the ticket product to use in your customizations.

In `src/admin/types.ts`, add the following interface at the end of the file:

```ts title="src/admin/types.ts"
export interface TicketProduct {
  id: string
  product_id: string
  venue_id: string
  dates: string[]
  venue: {
    id: string
    name: string
    address?: string
  }
  product: {
    id: string
    title: string
  }
  variants: Array<{
    id: string
    row_type: string
  }>
  created_at: string
  updated_at: string
}
```

You define the `TicketProduct` interface that describes the shape of a ticket product object.

### b. Create Ticket Products Page

Next, you'll create the UI route for the ticket products page.

Create the file `src/admin/routes/ticket-products/page.tsx` with the following content:

```tsx title="src/admin/routes/ticket-products/page.tsx" collapsibleLines="1-20" expandButtonLabel="Show Imports"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ReceiptPercent } from "@medusajs/icons"
import {
  createDataTableColumnHelper,
  Container,
  DataTable,
  useDataTable,
  Heading,
  DataTablePaginationState,
  Badge,
  toast,
} from "@medusajs/ui"
import { useQuery, useQueryClient } from "@tanstack/react-query"
import { Link } from "react-router-dom"
import React, { useState, useMemo } from "react"
import { sdk } from "../../lib/sdk"
import { TicketProduct } from "../../types"

const columnHelper = createDataTableColumnHelper<TicketProduct>()

const columns = [
  columnHelper.accessor("product.title", {
    header: "Name",
  }),
  columnHelper.accessor("venue.name", {
    header: "Venue",
  }),
  columnHelper.accessor("dates", {
    header: "Dates",
    cell: ({ row }) => {
      const dates = row.original.dates || []
      // Show first and last dates
      const displayDates = [dates[0], dates[dates.length - 1]]
      return (
        <div className="flex flex-wrap gap-1 items-center">
          {displayDates.map((date, index) => (
            <React.Fragment key={date}>
              <Badge color="grey" size="small">
                {new Date(date).toLocaleDateString()}
              </Badge>
              {index < displayDates.length - 1 && (
                <span className="text-gray-500 txt-small">
                  -
                </span>
              )}
            </React.Fragment>
          ))}
        </div>
      )
    },
  }),
  columnHelper.accessor("product_id", {
    header: "Product",
    cell: ({ row }) => {
      return (
        <Link to={`/products/${row.original.product_id}`}>
          View Product Details
        </Link>
      )
    },
  }),
]

const TicketProductsPage = () => {
  // TODO show table
}

export const config = defineRouteConfig({
  label: "Shows",
  icon: ReceiptPercent,
})

export default TicketProductsPage
```

First, you define the columns for the data table that will display ticket products. You create columns for the ticket product's name, venue, dates, and a link to view the associated Medusa product.

Then, you create the `TicketProductsPage` component and export a configuration object that defines the route's sidebar label and icon.

Next, you'll implement the `TicketProductsPage` component to show the table of ticket products. Replace the `TicketProductsPage` component with the following:

```tsx title="src/admin/routes/ticket-products/page.tsx"
const TicketProductsPage = () => {
  const limit = 15
  const [pagination, setPagination] = useState<DataTablePaginationState>({
    pageSize: limit,
    pageIndex: 0,
  })

  const queryClient = useQueryClient()

  const offset = useMemo(() => {
    return pagination.pageIndex * limit
  }, [pagination])

  const { data, isLoading } = useQuery<{
    ticket_products: TicketProduct[]
    count: number
    limit: number
    offset: number
  }>({
    queryKey: ["ticket-products", offset, limit],
    queryFn: () => sdk.client.fetch("/admin/ticket-products", {
      query: {
        offset: pagination.pageIndex * pagination.pageSize,
        limit: pagination.pageSize,
        order: "-created_at",
      },
    }),
  })

  const table = useDataTable({
    columns,
    data: data?.ticket_products || [],
    rowCount: data?.count || 0,
    isLoading,
    pagination: {
      state: pagination,
      onPaginationChange: setPagination,
    },
    getRowId: (row) => row.id,
  })

  return (
    <Container className="divide-y p-0">
      <DataTable instance={table}>
        <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
          <Heading>
            Shows
          </Heading>
        </DataTable.Toolbar>
        <DataTable.Table />
        <DataTable.Pagination />
      </DataTable>
    </Container>
  )
}
```

In the component, you define variables to manage pagination in the data table. You also use Tanstack Query and the JS SDK to retrieve the ticket products from the `GET /admin/ticket-products` API route you created earlier.

Then, you use Medusa UI's `DataTable` component to render the table of ticket products.

### c. Create Ticket Product Modal

Next, you'll create a modal component that allows creating a new ticket product. You'll show the modal when a button is clicked on the ticket products page.

The modal form is made up of two steps: one to select the venue and show dates, and another to set the prices of each row type. So, you'll create the components for each step first.

#### Create Product Details Step

To create the first step component, create the file `src/admin/components/product-details-step.tsx` with the following content:

```tsx title="src/admin/components/product-details-step.tsx" collapsibleLines="1-13" expandButtonLabel="Show Imports"
import { useState } from "react"
import {
  Input,
  Label,
  Select,
  DatePicker,
  Button,
  Text,
  Heading,
  Badge,
} from "@medusajs/ui"
import { XMark } from "@medusajs/icons"
import { Venue } from "../types"

interface ProductDetailsStepProps {
  name: string
  setName: (name: string) => void
  selectedVenueId: string
  setSelectedVenueId: (venueId: string) => void
  selectedDates: string[]
  setSelectedDates: (dates: string[]) => void
  venues: Venue[]
}

export const ProductDetailsStep = ({
  name,
  setName,
  selectedVenueId,
  setSelectedVenueId,
  selectedDates,
  setSelectedDates,
  venues,
}: ProductDetailsStepProps) => {
  const selectedVenue = venues.find((v) => v.id === selectedVenueId)
  
  // Local state for start and end dates
  const [startDate, setStartDate] = useState<Date | undefined>(
    selectedDates.length > 0 ? new Date(selectedDates[0] + "T00:00:00") : undefined
  )
  const [endDate, setEndDate] = useState<Date | undefined>(
    selectedDates.length > 1 ? new Date(selectedDates[selectedDates.length - 1] + "T00:00:00") : undefined
  )

  // TODO handle date selection
}
```

You define the `ProductDetailsStep` component that accepts props for managing the form state, including the ticket product name, selected venue, and selected dates.

In the component, you also define local state for the start and end dates used in the date picker.

Next, you'll add functions that handle selecting dates. Admins can select a start and end date, which will select the range of dates in between. Admins can also delete any date from the range.

Replace the `TODO` with the following:

```tsx title="src/admin/components/product-details-step.tsx"
const generateDateRange = (start: Date, end?: Date) => {
  const dates: string[] = []
  const currentDate = new Date(start)
  
  do {
    // Use local date formatting to avoid timezone issues
    const year = currentDate.getFullYear()
    const month = String(currentDate.getMonth() + 1).padStart(2, "0")
    const day = String(currentDate.getDate()).padStart(2, "0")
    dates.push(`${year}-${month}-${day}`)
    currentDate.setDate(currentDate.getDate() + 1)
  } while (end && currentDate <= end)
  
  return dates
}

const handleStartDateChange = (date: Date | null) => {
  const dateValue = date || undefined
  setStartDate(dateValue)
  setSelectedDates(
    dateValue ? generateDateRange(dateValue, endDate) : []
  )
}

const handleEndDateChange = (date: Date | null) => {
  const dateValue = date || undefined
  setEndDate(dateValue)
  if (startDate && dateValue) {
    setSelectedDates(generateDateRange(startDate, dateValue))
  } else if (dateValue) {
    setSelectedDates(generateDateRange(dateValue))
  } else {
    setSelectedDates([])
  }
}

const removeDate = (dateToRemove: string) => {
  setSelectedDates(selectedDates.filter((d) => d !== dateToRemove))
}

// TODO render form
```

You define the following functions:

- `generateDateRange`: Generates an array of date strings between a start and optional end date.
- `handleStartDateChange`: Handles changes to the start date, updating the selected dates accordingly.
- `handleEndDateChange`: Handles changes to the end date, updating the selected dates accordingly.
- `removeDate`: Removes a specific date from the selected dates.

Finally, you'll render the form for the product details step. Replace the `TODO` with the following:

```tsx title="src/admin/components/product-details-step.tsx"
return (
  <div className="space-y-6">
    <Heading level="h2">Show Details</Heading>
    <div>
      <Label htmlFor="name">Name</Label>
      <Input
        id="name"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Enter name"
      />
    </div>

    <div>
      <Label htmlFor="venue">Venue</Label>
      <Select
        value={selectedVenueId}
        onValueChange={setSelectedVenueId}
      >
        <Select.Trigger>
          <Select.Value placeholder="Select a venue" />
        </Select.Trigger>
        <Select.Content>
          {venues.map((venue) => (
            <Select.Item key={venue.id} value={venue.id}>
              {venue.name}
            </Select.Item>
          ))}
        </Select.Content>
      </Select>
    </div>

    {selectedVenue && (
      <div className="p-4 bg-gray-50 rounded-lg">
        <Text className="txt-small-plus mb-2">Selected Venue: {selectedVenue.name}</Text>
        {selectedVenue.address && (
          <Text className="txt-small text-ui-fg-subtle mb-2">{selectedVenue.address}</Text>
        )}
        <Text className="txt-small text-ui-fg-subtle">
          Rows: {[...new Set(selectedVenue.rows.map((row) => row.row_type))].join(", ")}<br/>
          Total Seats: {selectedVenue.rows.reduce((acc, row) => acc + row.seat_count, 0)}
        </Text>
      </div>
    )}

    <hr className="my-6" />

    <div>
      <Heading level="h2">Dates</Heading>
      <div className="mt-2 space-y-4">
        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
          <div>
            <Label htmlFor="start-date">Start Date</Label>
            <DatePicker
              value={startDate}
              onChange={handleStartDateChange}
              maxValue={endDate}
            />
          </div>
          <div>
            <Label htmlFor="end-date">End Date</Label>
            <DatePicker
              value={endDate}
              onChange={handleEndDateChange}
              minValue={startDate}
            />
          </div>
        </div>
        
        {selectedDates.length > 0 && (
          <div className="space-y-2">
            <Text className="txt-small-plus">
              Selected Dates ({selectedDates.length} day{selectedDates.length !== 1 ? "s" : ""}):
            </Text>
            <div className="flex flex-wrap gap-2">
              {selectedDates.map((date) => (
                <Badge
                  key={date}
                  color="blue"
                >
                  <span>{new Date(date).toLocaleDateString()}</span>
                  <Button
                    variant="transparent"
                    size="small"
                    onClick={() => removeDate(date)}
                    className="p-1 hover:bg-transparent"
                  >
                    <XMark />
                  </Button>
                </Badge>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  </div>
)
```

You render the form for the product details step, including inputs for the ticket product name, venue selection, and date selection.

#### Pricing Step

Next, you'll create the second step component for setting prices for each row type.

To create the step component, create the file `src/admin/components/pricing-step.tsx` with the following content:

```tsx title="src/admin/components/pricing-step.tsx" collapsibleLines="1-10" expandButtonLabel="Show Imports"
import {
  Input,
  Label,
  Text,
  Heading,
  Container,
  Badge,
} from "@medusajs/ui"
import { RowType, Venue } from "../types"

export interface CurrencyRegionCombination {
  currency: string
  region_id?: string
  region_name?: string
  is_store_currency: boolean
}

interface PricingStepProps {
  selectedVenue: Venue | undefined
  currencyRegionCombinations: CurrencyRegionCombination[]
  prices: Record<string, Record<string, number>>
  setPrices: (prices: Record<string, Record<string, number>>) => void
}

export const PricingStep = ({
  selectedVenue,
  currencyRegionCombinations,
  prices,
  setPrices,
}: PricingStepProps) => {
  if (!selectedVenue) {
    return (
      <div className="text-center py-8">
        <Text>Please select a venue in the previous step</Text>
      </div>
    )
  }

  // TODO add price and row type functions
}
```

You define the `PricingStep` component that accepts props for the selected venue, currency-region combinations, and prices.

In Medusa, you can set the price of a product variant in multiple currencies and regions. This allows you to sell products in different markets with localized pricing. Learn more in the [Pricing documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts/index.html.md).

In the component, if no venue is selected, you display a message prompting the user to select a venue in the previous step.

Next, you'll add functions for formatting and to handle price changes. Replace the `TODO` with the following:

```tsx title="src/admin/components/pricing-step.tsx"
const updatePrice = (
  rowType: string, 
  currency: string, 
  regionId: string | undefined, 
  amount: number
) => {
  const key = regionId ? `${currency}_${regionId}` : `${currency}_store`
  setPrices({
    ...prices,
    [rowType]: {
      ...prices[rowType],
      [key]: amount,
    },
  })
}

const getRowTypeColor = (
  type: RowType
): "purple" | "orange" | "blue" | "grey" => {
  switch (type) {
    case RowType.VIP:
      return "purple"
    case RowType.PREMIUM:
      return "orange"
    case RowType.BALCONY:
      return "blue"
    default:
      return "grey"
  }
}

const getRowTypeLabel = (type: RowType) => {
  switch (type) {
    case RowType.VIP:
      return "VIP"
    default:
      return type.charAt(0).toUpperCase() + type.slice(1)
  }
}

// Get unique row types from venue
const rowTypes = [...new Set(selectedVenue.rows.map((row) => row.row_type))]

// TODO render form
```

You define the following functions:

- `updatePrice`: Updates the price for a specific row type and currency-region combination.
- `getRowTypeColor`: Returns a color based on the row type for styling purposes.
- `getRowTypeLabel`: Returns a formatted label for the row type.

You also extract the unique row types from the selected venue to use in the form.

Finally, you'll render the form for the pricing step. Replace the `TODO` with the following:

```tsx title="src/admin/components/pricing-step.tsx"
return (
  <div className="space-y-6">
    <div>
      <Heading level="h3">Set Prices for Each Row Type</Heading>
      <Text className="text-ui-fg-subtle">
        Enter prices for each row type by region and currency. All prices are optional.
      </Text>
    </div>

    <div className="space-y-4">
      {rowTypes.map((rowType) => {
        const totalSeats = selectedVenue.rows
          .filter((row) => row.row_type === rowType)
          .reduce((sum, row) => sum + row.seat_count, 0)

        return (
          <Container key={rowType} className="p-4">
            <div className="flex items-center gap-3 mb-4">
              <Badge color={getRowTypeColor(rowType as RowType)} size="small">
                {getRowTypeLabel(rowType)}
              </Badge>
              <Text className="txt-small text-ui-fg-subtle">
                {totalSeats} seats total
              </Text>
            </div>

            <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
              {currencyRegionCombinations.map((combo) => {
                const key = combo.region_id ? `${combo.currency}_${combo.region_id}` : `${combo.currency}_store`
                return (
                  <div key={key}>
                    <Label htmlFor={`${rowType}-${key}`}>
                      {combo.currency.toUpperCase()} - {combo.region_name || "Store"}
                    </Label>
                    <Input
                      id={`${rowType}-${key}`}
                      type="number"
                      min="0"
                      step="0.01"
                      value={prices[rowType]?.[key] || ""}
                      onChange={(e) => {
                        const amount = parseFloat(e.target.value) || 0
                        updatePrice(rowType, combo.currency, combo.region_id, amount)
                      }}
                      placeholder="0.00"
                    />
                  </div>
                )
              })}
            </div>
          </Container>
        )
      })}
    </div>
  </div>
)
```

You render the form for the pricing step, including sections for each row type with inputs for setting prices in different currencies and regions.

#### Create Ticket Product Modal

You can now create the modal component that shows a multi-step form to create a ticket product.

Create the file `src/admin/components/create-ticket-product-modal.tsx` with the following content:

```tsx title="src/admin/components/create-ticket-product-modal.tsx" collapsibleLines="1-13" expandButtonLabel="Show Imports"
import React, { useState } from "react"
import {
  Button,
  FocusModal,
  ProgressTabs,
  toast,
} from "@medusajs/ui"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"
import { RowType, Venue } from "../types"
import { ProductDetailsStep } from "./product-details-step"
import { CurrencyRegionCombination, PricingStep } from "./pricing-step"

interface CreateTicketProductModalProps {
  open: boolean
  onOpenChange: (open: boolean) => void
  onSubmit: (data: any) => Promise<void>
}

export const CreateTicketProductModal = ({
  open,
  onOpenChange,
  onSubmit,
}: CreateTicketProductModalProps) => {
  const [currentStep, setCurrentStep] = useState("0")
  const [isLoading, setIsLoading] = useState(false)

  // Step 1 data
  const [name, setName] = useState("")
  const [selectedVenueId, setSelectedVenueId] = useState("")
  const [selectedDates, setSelectedDates] = useState<string[]>([])

  // Step 2 data - prices[rowType][currency_region] = amount
  const [prices, setPrices] = useState<Record<string, Record<string, number>>>({})

  // TODO fetch venues and currency-region combinations
}
```

You define the `CreateTicketProductModal` component that accepts props for managing the modal's open state and handling form submission.

In the component, you define state variables to manage the current step, loading state, and form data for both steps.

Next, you'll fetch the list of venues, regions, and store currencies from the Medusa backend. Replace the `TODO` with the following:

```tsx title="src/admin/components/create-ticket-product-modal.tsx"
// Fetch venues
const { data: venuesData } = useQuery<{
  venues: Venue[]
  count: number
}>({
  queryKey: ["venues"],
  queryFn: () => sdk.client.fetch("/admin/venues"),
})

// Fetch regions
const { data: regionsData } = useQuery({
  queryKey: ["regions"],
  queryFn: () => sdk.admin.region.list(),
})

// Fetch stores
const { data: storesData } = useQuery({
  queryKey: ["stores"],
  queryFn: () => sdk.admin.store.list(),
})

const venues = venuesData?.venues || []
const regions = regionsData?.regions || []
const stores = storesData?.stores || []
const selectedVenue = venues?.find((v) => v.id === selectedVenueId)

// TODO prepare currency-region combinations
```

You use Tanstack Query and the JS SDK to fetch the list of venues from the `GET /admin/venues` API route, and the list of regions and stores from admin API routes.

Next, you'll prepare the currency-region combinations based on the store and region data fetched. Replace the `TODO` with the following:

```tsx title="src/admin/components/create-ticket-product-modal.tsx"
const currencyRegionCombinations = React.useMemo(() => {
  const combinations: Array<CurrencyRegionCombination> = []
  
  // Add combinations from regions
  regions.forEach((region: any) => {
    combinations.push({
      currency: region.currency_code,
      region_id: region.id,
      region_name: region.name,
      is_store_currency: false,
    })
  })
  
  // Add combinations from stores (all supported currencies)
  stores.forEach((store) => {      
    // Add all supported currencies
    store.supported_currencies.forEach((currency) => {
      combinations.push({
        currency: currency.currency_code,
        region_id: undefined, // No region for store currencies
        is_store_currency: true,
      })
    })
  })
  
  return combinations
}, [regions, stores])

// TODO handle form actions
```

You create a memoized array of currency-region combinations by iterating over the fetched regions and stores. You add combinations for each region's currency and each store's supported currencies.

Next, you'll add functions to handle form actions like resetting the form or moving between steps. Replace the `TODO` with the following:

```tsx title="src/admin/components/create-ticket-product-modal.tsx"
const resetForm = () => {
  setName("")
  setSelectedVenueId("")
  setSelectedDates([])
  setPrices({})
  setCurrentStep("0")
}

const handleCloseModal = (open: boolean) => {
  if (!open) {
    resetForm()
  }
  onOpenChange(open)
}

const handleStep1Next = () => {
  if (!name.trim()) {
    toast.error("Name is required")
    return
  }
  if (!selectedVenueId) {
    toast.error("Please select a venue")
    return
  }
  if (selectedDates.length === 0) {
    toast.error("Please select at least one date")
    return
  }
  setCurrentStep("1")
}

const handleStep2Submit = async () => {
  if (!selectedVenue) {
    toast.error("Venue not found")
    return
  }

  // Prepare variants data
  // combine rows with the same row_type
  const combinedRows: Record<RowType, { seat_count: number }> = {
    premium: { seat_count: 0 },
    balcony: { seat_count: 0 },
    standard: { seat_count: 0 },
    vip: { seat_count: 0 },
  }
  selectedVenue.rows.forEach((row) => {
    if (!combinedRows[row.row_type]) {
      combinedRows[row.row_type] = { seat_count: 0 }
    }
    combinedRows[row.row_type].seat_count += row.seat_count
  })
  const variants = Object.keys(combinedRows).map((rowType) => ({
    row_type: rowType as RowType,
    seat_count: combinedRows[rowType as RowType].seat_count,
    prices: currencyRegionCombinations.map((combo) => {
      const key = combo.region_id ? `${combo.currency}_${combo.region_id}` : `${combo.currency}_store`
      const amount = prices[rowType as RowType]?.[key] || 0
      
      const price: any = {
        currency_code: combo.currency,
        amount: amount,
      }
      
      // Only add rules for region-based currencies
      if (combo.region_id && !combo.is_store_currency) {
        price.rules = {
          region_id: combo.region_id,
        }
      }
      
      return price
    }).filter((price) => price.amount > 0), // Only include prices > 0
  })).filter((variant) => variant.seat_count > 0) // Only create variants for row types with seats

  setIsLoading(true)
  try {
    await onSubmit({
      name,
      venue_id: selectedVenueId,
      dates: selectedDates,
      variants,
    })
    toast.success("Show created successfully")
    handleCloseModal(false)
  } catch (error: any) {
    toast.error(error.message || "Failed to create show")
  } finally {
    setIsLoading(false)
  }
}

// TODO define steps
```

You define the following functions:

- `resetForm`: Resets the form state to its initial values.
- `handleCloseModal`: Handles closing the modal and resets the form if it's being closed.
- `handleStep1Next`: Validates the inputs in the first step before moving to the next step.
- `handleStep2Submit`: Prepares the data from both steps and calls the `onSubmit` prop to create the ticket product.

Before submitting the form, you combine rows with the same `row_type` to create variants and prepare the prices for each variant based on the selected currency-region combinations.

Next, you'll define step variables useful to render the multi-step form. Replace the `TODO` with the following:

```tsx title="src/admin/components/create-ticket-product-modal.tsx"
// Check if step 1 (Product Details) is completed
const isStep1Completed = name.trim() && selectedVenueId && selectedDates.length > 0

// Check if step 2 (Pricing) is completed
const hasAnyPrices = Object.values(prices).some((rowPrices) => 
  Object.values(rowPrices).some((amount) => amount > 0)
)
const isStep2Completed = isStep1Completed && hasAnyPrices

const steps = [
  {
    label: "Product Details",
    value: "0",
    status: isStep1Completed ? "completed" as const : undefined,
    content: (
      <ProductDetailsStep
        name={name}
        setName={setName}
        selectedVenueId={selectedVenueId}
        setSelectedVenueId={setSelectedVenueId}
        selectedDates={selectedDates}
        setSelectedDates={setSelectedDates}
        venues={venues}
      />
    ),
  },
  {
    label: "Pricing",
    value: "1",
    status: isStep2Completed ? "completed" as const : undefined,
    content: (
      <PricingStep
        selectedVenue={selectedVenue}
        currencyRegionCombinations={currencyRegionCombinations}
        prices={prices}
        setPrices={setPrices}
      />
    ),
  },
]

// TODO render modal
```

You define variables to check if each step is completed based on the form inputs. You also define an array of step objects, each containing a label, value, status, and content component.

Finally, you'll render the modal with the multi-step form. Replace the `TODO` with the following:

```tsx title="src/admin/components/create-ticket-product-modal.tsx"
return (
  <FocusModal open={open} onOpenChange={handleCloseModal}>
    <FocusModal.Content>
      <FocusModal.Header className="justify-start py-0">
        <div className="flex flex-col gap-4 w-full">
          <ProgressTabs
            value={currentStep}
            onValueChange={setCurrentStep}
            className="w-full"
          >
            <ProgressTabs.List className="w-full">
              {steps.map((step) => (
                <ProgressTabs.Trigger 
                  key={step.value} 
                  value={step.value}
                  status={step.status}
                >
                  {step.label}
                </ProgressTabs.Trigger>
              ))}
            </ProgressTabs.List>
          </ProgressTabs>
        </div>
      </FocusModal.Header>
      <FocusModal.Body className="flex flex-1 flex-col p-6">
        <ProgressTabs
          value={currentStep}
          onValueChange={setCurrentStep}
          className="flex-1 w-full mx-auto"
        >
          {steps.map((step) => (
            <ProgressTabs.Content key={step.value} value={step.value} className="flex-1">
              <div className="max-w-[720px] mx-auto">
                {step.content}
              </div>
            </ProgressTabs.Content>
          ))}
        </ProgressTabs>
      </FocusModal.Body>
      <FocusModal.Footer>
        <Button
          variant="secondary"
          onClick={() => setCurrentStep(currentStep === "1" ? "0" : "0")}
          disabled={currentStep === "0"}
        >
          Previous
        </Button>
        
        {currentStep === "0" ? (
          <Button
            variant="primary"
            onClick={handleStep1Next}
          >
            Next
          </Button>
        ) : (
          <Button
            variant="primary"
            onClick={handleStep2Submit}
            isLoading={isLoading}
          >
            Create Show
          </Button>
        )}
      </FocusModal.Footer>
    </FocusModal.Content>
  </FocusModal>
)
```

You render the `FocusModal` component with a header containing progress tabs for navigation between steps, a body displaying the content of the current step, and a footer with buttons to navigate between steps or submit the form.

### d. Add Modal to Ticket Products Page

You can now render the `CreateTicketProductModal` component in the `TicketProductsPage` component.

In `src/admin/routes/ticket-products/page.tsx`, add the following imports at the top of the file:

```tsx title="src/admin/routes/ticket-products/page.tsx"
import {
  Button,
} from "@medusajs/ui"
import { CreateTicketProductModal } from "../../components/create-ticket-product-modal"
```

Then, in the `TicketProductsPage` component, add the following state variable to manage the modal's open state:

```tsx title="src/admin/routes/ticket-products/page.tsx"
const TicketProductsPage = () => {
  const [isModalOpen, setIsModalOpen] = useState(false)
  // ...
}
```

Next, before the return statement, add the following functions to handle closing the modal and submitting the form:

```tsx title="src/admin/routes/ticket-products/page.tsx"
const TicketProductsPage = () => {
  // ...
  const handleCloseModal = () => {
    setIsModalOpen(false)
  }

  const handleCreateTicketProduct = async (data: any) => {
    try {
      await sdk.client.fetch("/admin/ticket-products", {
        method: "POST",
        body: data,
      })
      queryClient.invalidateQueries({ queryKey: ["ticket-products"] })
      handleCloseModal()
    } catch (error: any) {
      toast.error(`Failed to create show: ${error.message}`)
    }
  }
  // ...
}
```

You define the `handleCloseModal` function to close the modal, and the `handleCreateTicketProduct` function to submit the form data to the `POST /admin/ticket-products` API route you created earlier.

Finally, in the return statement, add a button to open the modal as the last child of `DataTable.Toolbar`:

```tsx title="src/admin/routes/ticket-products/page.tsx"
return (
  <Container className="divide-y p-0">
    {/* ... */}
      <Button
        variant="secondary"
        onClick={() => setIsModalOpen(true)}
      >
        Create Show
      </Button>
    {/* ... */}
  </Container>
)
```

And add the `CreateTicketProductModal` component as the last child of the `Container` component:

```tsx title="src/admin/routes/ticket-products/page.tsx"
return (
  <Container className="divide-y p-0">
    {/* ... */}
    <CreateTicketProductModal
      open={isModalOpen}
      onOpenChange={handleCloseModal}
      onSubmit={handleCreateTicketProduct}
    />
  </Container>
)
```

### Test the Ticket Products Page

You can now test the ticket products page in the Medusa Admin.

Start the Medusa server if it isn't already running. If you open the Medusa Admin and log in, you should see a new "Shows" item in the sidebar. If you click on it, you can see a table of ticket products with a button to create a new show.

![Shows page with the table of ticket products and a button to create a new show](https://res.cloudinary.com/dza7lstvk/image/upload/v1757490157/Medusa%20Resources/CleanShot_2025-09-10_at_10.42.00_2x_jsaq7v.png)

To create a new show (or ticket product):

1. Click the "Create Show" button to open the modal.
2. In the "Product Details" step, enter a name for the show, select a venue, and select one or more dates using the date pickers.
3. Click "Next" to go to the "Pricing" step.

![Create Show modal with the Product Details step](https://res.cloudinary.com/dza7lstvk/image/upload/v1757490395/Medusa%20Resources/CleanShot_2025-09-10_at_10.46.23_2x_v2pbuo.png)

4. In the "Pricing" step, set prices for each row type. You can set prices in different currencies and regions. All prices are optional.
5. Once you're done, click "Create Show" to submit the form.

![Create Show modal with the Pricing step](https://res.cloudinary.com/dza7lstvk/image/upload/v1757490725/Medusa%20Resources/CleanShot_2025-09-10_at_10.51.55_2x_bijkab.png)

This will create a new ticket product with a Medusa product. You can view the ticket product in the table, and you can click the link to view the associated Medusa product.

You can edit the associated Medusa product to add images, descriptions, and other details for the show.

***

## Step 10: Validate Ticket on Add to Cart

In this step, you'll add custom validation to the core add-to-cart operation that ensures a seat isn't purchased more than once for the same date.

Medusa implements cart operations in workflows. Specifically, you'll focus on the `addToCartWorkflow`. Medusa allows you to inject custom logic into workflows using [hooks](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md).

A workflow hook is a point in a workflow where you can inject custom functionality as a step function.

To consume the `validate` hook of the `addToCartWorkflow` that holds the add-to-cart logic, create the file `src/workflows/hooks/add-to-cart-validation.ts` with the following content:

```ts title="src/workflows/hooks/add-to-cart-validation.ts"
import { addToCartWorkflow } from "@medusajs/medusa/core-flows"
import { MedusaError } from "@medusajs/framework/utils"

// Hook for addToCartWorkflow to validate seat availability
addToCartWorkflow.hooks.validate(
  async ({ input }, { container }) => {
    const items = input.items
    const query = container.resolve("query")

    // Get the product variant to check if it's a ticket product variant
    const { data: productVariants } = await query.graph({
      entity: "product_variant",
      fields: ["id", "product_id", "ticket_product_variant.purchases.*"],
      filters: {
        id: items.map((item) => item.variant_id).filter(Boolean) as string[],
      },
    })

    // Get existing cart items to check for conflicts
    const { data: [cart] } = await query.graph({
      entity: "cart",
      fields: ["items.*"],
      filters: {
        id: input.cart_id,
      },
    }, {
      throwIfKeyNotFound: true,
    })

    // Check each item being added to cart
    for (const item of items) {
      if (item.quantity !== 1) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA, 
          "You can only purchase one ticket for a seat."
        )
      }
      const productVariant = productVariants.find(
        (variant) => variant.id === item.variant_id
      )

      if (!productVariant || !item.metadata?.seat_number) {continue}

      if (!item.metadata?.show_date) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          `Show date is required for seat ${item.metadata?.seat_number} in product ${productVariant.product_id}`
        )
      }

      // Check if seat has already been purchased
      const existingPurchase = productVariant.ticket_product_variant?.purchases.find(
        (purchase) => purchase?.seat_number === item.metadata?.seat_number 
          && purchase?.show_date === item.metadata?.show_date
      )

      if (existingPurchase) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          `Seat ${item.metadata?.seat_number} has already been purchased for show date ${item.metadata?.show_date}`
        )
      }

      // Check if seat is already in the cart
      const existingCartItem = cart.items.find(
        (cartItem) => cartItem?.metadata?.seat_number === item.metadata?.seat_number 
          && cartItem?.metadata?.show_date === item.metadata?.show_date
      )

      if (existingCartItem) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          `Seat ${item.metadata?.seat_number} is already in your cart for show date ${item.metadata?.show_date}`
        )
      }
    }
  }
)
```

You consume the hook by calling `addToCartWorkflow.hooks.validate`, passing it a step function.

In the step function, you:

- Retrieve the product variants being added to the cart with their associated ticket product variant purchases.
- Retrieve the existing cart items to check for conflicts.
- Throw an error if:
  - The quantity of any item being added is more than 1 (you can only purchase one ticket for a seat).
  - The show date is missing in the item metadata.
  - The seat has already been purchased for the same show date.
  - The seat is already in the cart for the same show date.

If the hook throws an error, the add-to-cart operation will be aborted and the error message will be returned to the client.

You can test out the hook when you [customize the storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/ticket-booking/example/storefront/index.html.md).

***

## Step 11: Custom Complete Cart

In this step, you'll create a custom complete cart workflow that wraps the default `completeCartWorkflow` to add logic that creates ticket purchases for each ticket product variant in the cart. Then, you'll execute that workflow in a custom API route.

### a. Custom Complete Cart Workflow

The custom workflow that completes the cart has the following steps:

- [acquireLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/acquireLockStep/index.html.md): Acquire a lock on the cart to prevent concurrent modifications
- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md): Complete the cart using Medusa's default completeCartWorkflow
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart details
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve existing ticket purchases to ensure idempotency
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the order details
- [releaseLockStep](https://docs.medusajs.com/references/medusa-workflows/steps/releaseLockStep/index.html.md): Release the lock on the cart

You only need to implement the `createTicketPurchasesStep` and `validateTicketOrderStep` steps, as the other steps and workflows are provided by Medusa.

#### createTicketPurchasesStep

The `createTicketPurchasesStep` creates ticket purchases for each ticket product variant in a cart.

To create the step, create the file `src/workflows/steps/create-ticket-purchases.ts` with the following content:

```ts title="src/workflows/steps/create-ticket-purchases.ts" collapsibleLines="1-10" expandButtonLabel="Show Imports"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { 
  CartDTO, 
  CartLineItemDTO, 
  ProductVariantDTO,
  InferTypeOf,
} from "@medusajs/framework/types"
import { TICKET_BOOKING_MODULE } from "../../modules/ticket-booking"
import TicketProductVariant from "../../modules/ticket-booking/models/ticket-product-variant"

export type CreateTicketPurchasesStepInput = {
  order_id: string
  cart: CartDTO & {
    items: CartLineItemDTO & {
      variant?: ProductVariantDTO & {
        ticket_product_variant?: InferTypeOf<typeof TicketProductVariant>
      }
    }[]
  }
}

export const createTicketPurchasesStep = createStep(
  "create-ticket-purchases",
  async (input: CreateTicketPurchasesStepInput, { container }) => {
    const { order_id, cart } = input
    const ticketBookingModuleService = container.resolve(TICKET_BOOKING_MODULE)

    const ticketPurchasesToCreate: {
      order_id: string
      ticket_product_id: string
      ticket_variant_id: string
      venue_row_id: string
      seat_number: string
      show_date: Date
    }[] = []

    // Process each item in the cart
    for (const item of cart.items) {
      if (
        !item?.variant?.ticket_product_variant || 
        !item?.metadata?.venue_row_id || 
        !item?.metadata?.seat_number
      ) {continue}

      ticketPurchasesToCreate.push({
        order_id,
        ticket_product_id: item.variant.ticket_product_variant.ticket_product_id,
        ticket_variant_id: item.variant.ticket_product_variant.id,
        venue_row_id: item?.metadata.venue_row_id as string,
        seat_number: item?.metadata.seat_number as string,
        show_date: new Date(
          item?.variant.options.find(
            (option: any) => option.option.title === "Date"
          )?.value as string
        ),
      })
    }

    const ticketPurchases = await ticketBookingModuleService.createTicketPurchases(
      ticketPurchasesToCreate
    )
    
    return new StepResponse(
      ticketPurchases,
      ticketPurchases
    )
  },
  async (ticketPurchases, { container }) => {
    if (!ticketPurchases) {return}

    const ticketBookingModuleService = container.resolve(TICKET_BOOKING_MODULE)

    // Delete the created ticket purchases
    await ticketBookingModuleService.deleteTicketPurchases(
      ticketPurchases.map((ticketPurchase) => ticketPurchase.id)
    )
  }
)
```

The `createTicketPurchasesStep` accepts the cart and order ID as input.

In the step function, you prepare the ticket purchases to be created, create the ticket purchases, and return them.

In the compensation function, you delete the created ticket purchases if an error occurs in the workflow.

#### validateTicketOrderStep

The `validateTicketOrderStep` validates that the tickets can be purchased based on their availability.

To create the step, create the file `src/workflows/steps/validate-ticket-order.ts` with the following content:

```ts title="src/workflows/steps/validate-ticket-order.ts"
import { MedusaError } from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { cancelOrderWorkflow } from "@medusajs/medusa/core-flows"

export type ValidateTicketOrderStepInput = {
  items: {
    id: string
    variant_id: string
    metadata: Record<string, unknown>
    quantity: number
    variant?: {
      id: string
      product_id: string
      ticket_product_variant?: {
        purchases?: {
          seat_number: string
          show_date: Date
        }[]
      }
    }
  }[]
  order_id: string
}

export const validateTicketOrderStep = createStep(
  "validate-ticket-order",
  async ({ items, order_id }: ValidateTicketOrderStepInput, { container }) => {
    // Check for duplicate seats within the cart
    const seatDateCombinations = new Set<string>()
    
    for (const item of items) {
      if (item.quantity !== 1) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA, 
          "You can only purchase one ticket for a seat."
        )
      }

      if (!item.variant || !item.metadata?.seat_number) {continue}

      if (!item.metadata?.show_date) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA, 
          `Show date is required for seat ${item.metadata?.seat_number} in product ${item.variant.product_id}`
        )
      }

      // Create a unique key for seat and date combination
      const seatDateKey = `${item.metadata?.seat_number}-${item.metadata?.show_date}`
      
      // Check if this seat-date combination already exists in the cart
      if (seatDateCombinations.has(seatDateKey)) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA, 
          `Duplicate seat ${item.metadata?.seat_number} found for show date ${item.metadata?.show_date} in cart`
        )
      }
      
      // Add to the set to track this combination
      seatDateCombinations.add(seatDateKey)

      // Check if seat has already been purchased
      const existingPurchase = item.variant.ticket_product_variant?.purchases?.find(
        (purchase) => purchase?.seat_number === item.metadata?.seat_number 
          && purchase?.show_date === item.metadata?.show_date
      )

      if (existingPurchase) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA, 
          `Seat ${item.metadata?.seat_number} has already been purchased for show date ${item.metadata?.show_date}`
        )
      }
    }

    return new StepResponse({ validated: true }, order_id)
  },
  async (order_id, { container, context }) => {
    if (!order_id) {return}

    cancelOrderWorkflow(container).run({
      input: {
        order_id,
      },
      context,
      container,
    })
  }
)
```

The `validateTicketOrderStep` accepts the cart items and order ID as input.

In the step function, you validate that:

1. No seat is purchased more than once for the same date within the cart.
2. No seat has already been purchased for the same date.

If any validation fails, you throw an error to abort the workflow.

The step also has a compensation function that cancels the order if an error occurs later in the workflow. This is important to ensure that if ticket purchase creation fails, the order is not left in a completed state.

#### Custom Complete Cart Workflow

You can now create the custom workflow that completes the cart and creates ticket purchases.

Create the file `src/workflows/complete-cart-with-tickets.ts` with the following content:

```ts title="src/workflows/complete-cart-with-tickets.ts" collapsibleLines="1-14" expandButtonLabel="Show Imports"
import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { 
  completeCartWorkflow, 
  createRemoteLinkStep, 
  acquireLockStep, 
  releaseLockStep, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { createTicketPurchasesStep, CreateTicketPurchasesStepInput } from "./steps/create-ticket-purchases"
import { TICKET_BOOKING_MODULE } from "../modules/ticket-booking"
import { Modules } from "@medusajs/framework/utils"
import ticketPurchaseOrderLink from "../links/ticket-purchase-order"
import { validateTicketOrderStep, ValidateTicketOrderStepInput } from "./steps/validate-ticket-order"

export type CompleteCartWithTicketsWorkflowInput = {
  cart_id: string
}

export const completeCartWithTicketsWorkflow = createWorkflow(
  "complete-cart-with-tickets",
  (input: CompleteCartWithTicketsWorkflowInput) => {
    acquireLockStep({
      key: input.cart_id,
      timeout: 2,
      ttl: 10,
    })
    const order = completeCartWorkflow.runAsStep({
      input: {
        id: input.cart_id,
      },
    })

    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      fields: [
        "id", 
        "items.variant.*",
        "items.variant.options.*",
        "items.variant.options.option.*",
        "items.variant.ticket_product_variant.*",
        "items.variant.ticket_product_variant.ticket_product.*",
        "items.variant.ticket_product_variant.purchases.*",
        "items.metadata",
        "items.quantity",
      ],
      filters: {
        id: input.cart_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const { data: existingLinks } = useQueryGraphStep({
      entity: ticketPurchaseOrderLink.entryPoint,
      fields: ["ticket_purchase.id"],
      filters: { order_id: order.id },
    }).config({ name: "retrieve-existing-links" })

    when({ existingLinks }, (data) => data.existingLinks.length === 0)
    .then(() => {
      validateTicketOrderStep({
        items: carts[0].items,
        order_id: order.id,
      } as unknown as ValidateTicketOrderStepInput)
      const ticketPurchases = createTicketPurchasesStep({
        order_id: order.id,
        cart: carts[0],
      } as unknown as CreateTicketPurchasesStepInput)
  
      const linkData = transform({
        order,
        ticketPurchases,
      }, (data) => {
        return data.ticketPurchases.map((purchase) => ({
          [TICKET_BOOKING_MODULE]: {
            ticket_purchase_id: purchase.id,
          },
          [Modules.ORDER]: {
            order_id: data.order.id,
          },
        }))
      })
  
      createRemoteLinkStep(linkData)
    })

    const { data: refetchedOrder } = useQueryGraphStep({
      entity: "order",
      fields: [
        "id",
        "currency_code",
        "email",
        "customer.*",
        "billing_address.*",
        "payment_collections.*",
        "items.*",
        "total",
        "subtotal",
        "tax_total",
        "shipping_total",
        "discount_total",
        "created_at",
        "updated_at",
      ],
      filters: {
        id: order.id,
      },
    }).config({ name: "refetch-order" })

    releaseLockStep({
      key: input.cart_id,
    })

    return new WorkflowResponse({
      order: refetchedOrder[0],
    })
  }
)
```

The `completeCartWithTicketsWorkflow` accepts the cart ID as input.

In the workflow function, you:

1. Acquire a lock on the cart to prevent concurrent modifications using the `acquireLockStep`.
2. Complete the cart using Medusa's `completeCartWorkflow`.
3. Retrieve the cart details using the `useQueryGraphStep`.
4. Retrieve existing ticket purchases linked to the order to ensure idempotency using the `useQueryGraphStep`.
   - This is important because if the workflow is retried, you don't want to create duplicate ticket purchases.
5. Use `when` to check that there are no existing links between the order and ticket purchases. If so, you:
   1. Validate that the ticket order can be processed using the `validateTicketOrderStep`.
   2. Create ticket purchases for each ticket product variant in the cart using the `createTicketPurchasesStep`.
   3. Create links between the order and the created ticket purchases using the `createRemoteLinkStep`.
6. Retrieve the order details using the `useQueryGraphStep`.
7. Release the lock on the cart using the `releaseLockStep`.

Finally, you return the order details.

### b. Custom Complete Cart API Route

Next, you'll create a custom API route that executes the `completeCartWithTicketsWorkflow` to complete the cart and create ticket purchases.

Create the file `src/api/store/carts/[id]/complete-tickets/route.ts` with the following content:

```ts title="src/api/store/carts/[id]/complete-tickets/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { completeCartWithTicketsWorkflow } from "../../../../../workflows/complete-cart-with-tickets"

export async function POST(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await completeCartWithTicketsWorkflow(req.scope).run({
    input: {
      cart_id: req.params.id,
    },
  })

  res.json({
    type: "order",
    order: result.order,
  })
}
```

Since you export a `POST` route handler function, you expose a `POST` API route at `/store/carts/{id}/complete-tickets`.

In the route handler, you execute the `completeCartWithTicketsWorkflow` and return the created order in the response.

You'll test out this API route when you [customize the storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/ticket-booking/example/storefront#update-cart-functions/index.html.md).

***

## Step 12: Storefront API Routes

To [customize the storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/ticket-booking/example/storefront/index.html.md) in the next part, you need two API routes that the storefront will consume:

1. An API route to fetch the available dates for a ticket product.
2. An API route to fetch the seating layout for a venue, including details on which seats are already booked for a specific date.

You'll test these API routes when you customize the storefront.

### a. Available Dates API Route

The first API route you'll create fetches the available dates for a ticket product.

Create the file `src/api/store/ticket-products/[id]/availability/route.ts` with the following content:

```ts title="src/api/store/ticket-products/[id]/availability/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"

export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
  const { id } = req.params
  const query = req.scope.resolve("query")

  const { data: [ticketProduct] } = await query.graph({
    entity: "ticket_product",
    fields: [
      "id",
      "product_id",
      "dates",
      "venue.*",
      "venue.rows.*",
      "variants.*",
      "variants.product_variant.*",
      "variants.product_variant.options.*",
      "variants.product_variant.options.option.*",
      "variants.product_variant.ticket_product_variant.*",
      "variants.product_variant.ticket_product_variant.purchases.*",
    ],
    filters: {
      product_id: id,
    },
  })

  if (!ticketProduct) {
    throw new MedusaError(MedusaError.Types.NOT_FOUND, "Ticket product not found")
  }

  // Calculate availability for each date and row type
  const availability = ticketProduct.dates.map((date: string) => {
    // Group rows by row_type to get total seats per row type
    const rowTypeGroups = ticketProduct.venue.rows.reduce((groups: any, row: any) => {
      if (!groups[row.row_type]) {
        groups[row.row_type] = {
          row_type: row.row_type,
          total_seats: 0,
          rows: [],
        }
      }
      groups[row.row_type].total_seats += row.seat_count
      groups[row.row_type].rows.push(row)
      return groups
    }, {})

    const dateAvailability = {
      date,
      row_types: Object.values(rowTypeGroups).map((group: any) => {
        // Find the variant for this date and row type
        const variant = ticketProduct.variants.find((v: any) => {
          const variantDate = v.product_variant.options.find((opt: any) => 
            opt.option?.title === "Date"
          )?.value
          const variantRowType = v.product_variant.options.find((opt: any) => 
            opt.option?.title === "Row Type"
          )?.value
          
          return variantDate === date && variantRowType === group.row_type
        })

        if (!variant) {
          return {
            row_type: group.row_type,
            total_seats: group.totalSeats,
            available_seats: 0,
            soldOut: true,
          }
        }

        // Count purchased seats for this variant
        const purchasedSeats = variant.product_variant?.ticket_product_variant?.purchases?.length || 0
        const availableSeats = Math.max(0, group.total_seats - purchasedSeats)
        const soldOut = availableSeats === 0

        return {
          row_type: group.row_type,
          total_seats: group.total_seats,
          available_seats: availableSeats,
          sold_out: soldOut,
        }
      }),
    }

    // Check if the entire date is sold out
    const totalAvailableSeats = dateAvailability.row_types.reduce(
      (sum, rowType) => sum + rowType.available_seats, 0
    )
    const dateSoldOut = totalAvailableSeats === 0

    return {
      ...dateAvailability,
      sold_out: dateSoldOut,
    }
  })

  return res.json({
    ticket_product: ticketProduct,
    availability,
  })
}
```

You expose a `GET` API route at `/store/ticket-products/{id}/availability`.

In the route handler, you:

1. Retrieve the ticket product by its associated Medusa product ID, including its dates, venue, variants, and purchases.
2. For each show date, you group the venue rows by `row_type` to calculate the total seats per row type.
3. For each row type, you find the corresponding variant for the date and row type, count the purchased seats, and calculate the available seats.
4. You also determine if a row type or an entire date is sold out.
5. Finally, you return the ticket product and its availability in the response.

### b. Seating Layout API Route

The second API route you'll create fetches the seating layout for a venue, including details on which seats are already booked for a specific date.

Create the file `src/api/store/ticket-products/[id]/seats/route.ts` with the following content:

```ts title="src/api/store/ticket-products/[id]/seats/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
import { z } from "zod"

export const GetTicketProductSeatsSchema = z.object({
  date: z.string(),
})

export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
  const { id } = req.params
  const { date } = req.validatedQuery
  const query = req.scope.resolve("query")

  const { data: [ticketProduct] } = await query.graph({
    entity: "ticket_product",
    fields: [
      "id",
      "product_id",
      "venue.*",
      "venue.rows.*",
      "variants.*",
      "variants.product_variant.*",
      "variants.product_variant.options.*",
      "variants.product_variant.options.option.*",
      "variants.product_variant.ticket_product_variant.*",
      "variants.product_variant.ticket_product_variant.purchases.*",
    ],
    filters: {
      product_id: id,
    },
  })

  if (!ticketProduct) {
    throw new MedusaError(MedusaError.Types.NOT_FOUND, "Ticket product not found")
  }

  // Build seat map for the specified date
  const seatMap = ticketProduct.venue.rows.map((row: any) => {
    // Find the variant for this date and row type
    const variant = ticketProduct.variants.find((v: any) => {
      const variantDate = v.product_variant.options.find((opt: any) => 
        opt.option?.title === "Date"
      )?.value
      const variantRowType = v.product_variant.options.find((opt: any) => 
        opt.option?.title === "Row Type"
      )?.value
      
      return variantDate === date && variantRowType === row.row_type
    })

    // Get purchased seats for this variant
    const purchasedSeats = variant?.product_variant?.ticket_product_variant?.purchases?.map(
      (purchase) => purchase?.seat_number
    ).filter(Boolean) || []

    // Generate seat numbers for this row
    const seats = Array.from({ length: row.seat_count }, (_, index) => {
      const seatNumber = (index + 1).toString()
      const isPurchased = purchasedSeats.includes(seatNumber)
      
      return {
        number: seatNumber,
        is_purchased: isPurchased,
        variant_id: variant?.product_variant?.id || null,
      }
    })

    return {
      row_number: row.row_number,
      row_type: row.row_type,
      seats,
    }
  })

  return res.json({
    venue: ticketProduct.venue,
    date,
    seat_map: seatMap,
  })
}
```

You expose a `GET` API route at `/store/ticket-products/{id}/seats`. You also define a Zod schema to validate the `date` query parameter.

In the route handler, you:

1. Retrieve the ticket product by its associated Medusa product ID, including its venue, rows, variants, and purchases.
2. For each venue row, you find the corresponding variant for the specified date and row type.
3. You get the purchased seats for that variant and generate a list of seats for the row, marking which seats are already purchased.
4. Finally, you return the venue details, the specified date, and the seat map in the response.

You also need to apply a middleware to validate the query parameters using the Zod schema you defined. So, in `src/api/middlewares.ts`, add the following import at the top of the file:

```ts title="src/api/middlewares.ts"
import { GetTicketProductSeatsSchema } from "./store/ticket-products/[id]/seats/route"
```

Then, pass a new object to the `routes` array passed to the `defineMiddlewares` function:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/store/ticket-products/:id/seats",
      methods: ["GET"],
      middlewares: [validateAndTransformQuery(GetTicketProductSeatsSchema, {})],
    },
  ],
})
```

You apply the `validateAndTransformQuery` middleware to the `/store/ticket-products/:id/seats` route for `GET` requests. You pass the `GetTicketProductSeatsSchema` to validate the query parameters.

***

## Step 13: Send Order Confirmation Email with Tickets

The last step is to send order confirmation emails to customers with their tickets as QR codes.

To send an email in Medusa when an order is placed, you'll need:

- A [Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) to handle sending emails. For example, [SendGrid](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md) or [Resend](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/integrations/guides/resend/index.html.md).
  - You also need to define an email template for the order confirmation email in the provider.
- A method in the Ticket Booking Module's service that generates the ticket QR codes.
- A [Subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that listens to the `order.placed` event and sends an email with the order details.
- A workflow and API route to handle verifying scanned QR codes at the event entrance.

### a. Set Up Notification Module

First, set up a Notification Module Provider in your Medusa application.

For example, to set up the SendGrid Notification Module Provider, add it to `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    // ...
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/notification-sendgrid",
            id: "sendgrid",
            options: {
              channels: ["email"],
              api_key: process.env.SENDGRID_API_KEY,
              from: process.env.SENDGRID_FROM,
            },
          },
        ],
      },
    },
  ],
})
```

Make sure to set the `SENDGRID_API_KEY` and `SENDGRID_FROM` environment variables in your `.env` file, as explained in the [SendGrid Notification Module Provider guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md).

You also need to define an email template for the order confirmation email in SendGrid. The following is a dynamic template you can use:

```html
<!doctype html>
<html lang="en" style="margin:0;padding:0;">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Order Confirmation – {{brand_name}}</title>
  <style>
    /* Dark mode friendly neutrals, safe for most clients */
    :root { color-scheme: light dark; supported-color-schemes: light dark; }
    body { margin:0; padding:0; background:#f6f7f9; -webkit-text-size-adjust:100%; }
    .wrapper { width:100%; table-layout:fixed; background:#f6f7f9; padding:24px 0; }
    .container { margin:0 auto; width:100%; max-width:640px; background:#ffffff; border-radius:12px; overflow:hidden; }
    .header { padding:24px; text-align:left; background:#0e1116; color:#ffffff; }
    .brand { font: 600 20px/1.2 system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, sans-serif; }
    .content { padding:24px; color:#0e1116; font: 400 16px/1.5 system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, sans-serif; }
    .muted { color:#6b7280; font-size:14px; }
    .section-title { font-weight:600; margin:24px 0 8px; }
    .card { border:1px solid #e5e7eb; border-radius:12px; padding:16px; }
    .divider { height:1px; background:#e5e7eb; margin:24px 0; }
    .btn {
      display:inline-block; text-decoration:none; background:#0e1116; color:#ffffff !important;
      padding:12px 18px; border-radius:10px; font-weight:600;
    }
    /* QR grid */
    .qr-grid { display:grid; grid-template-columns: repeat(2, 1fr); gap:16px; }
    @media (max-width:480px) { .qr-grid { grid-template-columns: 1fr; } }
    .qr-item { border:1px solid #e5e7eb; border-radius:10px; padding:14px; text-align:center; }
    .qr-item img { display:block; margin:0 auto 10px; width:220px; height:220px; object-fit:contain; }
    .qr-label { font-weight:600; margin-bottom:2px; }
    .qr-meta { font-size:13px; color:#6b7280; }
    .ticket-note { font-size:13px; color:#374151; margin-top:8px; }
    /* Small print */
    .footer { padding:18px 24px 28px; text-align:center; color:#6b7280; font-size:12px; }
    .nowrap { white-space:nowrap; }
    a { color:#0e63ff; }
  </style>
</head>
<body>
  <table role="presentation" class="wrapper" cellpadding="0" cellspacing="0" width="100%">
    <tr>
      <td align="center">
        <table role="presentation" class="container" cellpadding="0" cellspacing="0" width="100%">
          <!-- Header -->
          <tr>
            <td class="header">
              <div class="brand">Medusa</div>
            </td>
          </tr>

          <!-- Greeting & Summary -->
          <tr>
            <td class="content">
              <p>Hi {{customer.first_name}},</p>
              <p>Thanks for your order! Your tickets for <strong>{{show.name}}</strong> are confirmed.</p>

              <div class="card">
                <div><strong>Order #:</strong> {{order.display_id}}</div>
                <div><strong>Date:</strong> {{order.created_at}}</div>
                {{#if order.email}}
                  <div><strong>Sent to:</strong> {{order.email}}</div>
                {{/if}}
                {{#if show.date}}
                  <div><strong>Event:</strong> {{show.date}}</div>
                {{/if}}
                {{#if show.venue}}
                  <div><strong>Venue:</strong> {{show.venue}}</div>
                {{/if}}
              </div>

              <div class="divider"></div>

              <!-- QR Codes -->
              <h3 class="section-title">Your Tickets (QR Codes)</h3>
              <p class="muted" style="margin-top:0;">Show each QR code at the entrance. Each QR admits one person unless noted otherwise.</p>

              <div class="qr-grid">
                {{#each tickets}}
                  <div class="qr-item">
                    <!-- `qr` can be a full https URL or a data URI (e.g. data:image/png;base64,...) -->
                    <img src="{{this.qr}}" alt="QR code for {{this.label}}" width="220" height="220" />
                    <div class="qr-label">{{this.label}}</div>
                    {{#if this.seat}}
                      <div class="qr-meta">Seat: {{this.seat}}</div>
                    {{/if}}
                    {{#if this.row}}
                      <div class="qr-meta">Row: {{this.row}}</div>
                    {{/if}}
                  </div>
                {{/each}}
              </div>

              <p class="muted" style="margin-top:18px;">Please arrive at least 15 minutes before showtime and have your tickets ready.</p>

              <div class="divider"></div>

              <!-- Billing snippet -->
              <h3 class="section-title">Billing</h3>
                <div class="card">
                  {{#if billing_address}}
                    <div><strong>Billing address</strong><br/>
                      {{billing_address.first_name}} {{billing_address.last_name}}<br/>
                      {{billing_address.address_1}}{{#if billing_address.address_2}}, {{billing_address.address_2}}{{/if}}<br/>
                      {{billing_address.city}}, {{billing_address.province}} {{billing_address.postal_code}}<br/>
                      {{billing_address.country_code}}
                    </div>
                  {{/if}}
                </div>

              <p style="margin-top:24px;">Enjoy the show!</p>
            </td>
          </tr>
        </table>
      </td>
    </tr>
  </table>
</body>
</html>
```

You can customize the template to fit your brand and requirements. If you change any variable names or add new variables, make sure to update the code in the next sections accordingly.

### b. Generate Ticket QR Codes

Next, you'll add a method in the Ticket Booking Module's service that generates the ticket QR codes.

Start by installing the `qrcode` package in your Medusa application with the following command:

```bash npm2yarn
npm install qrcode
npm install --save-dev @types/qrcode
```

Then, in `src/modules/ticket-booking/service.ts`, add the following imports at the top of the file:

```ts title="src/modules/ticket-booking/service.ts"
import { promiseAll } from "@medusajs/framework/utils"
import QRCode from "qrcode"
```

And add the following method to `TicketBookingModuleService`:

```ts title="src/modules/ticket-booking/service.ts"
export class TicketBookingModuleService extends MedusaService({
  Venue,
  VenueRow,
  TicketProduct,
  TicketProductVariant,
  TicketPurchase,
}) {
  async generateTicketQRCodes(
    ticketPurchaseIds: string[]
  ): Promise<Record<string, string>> {
    const ticketPurchases = await this.listTicketPurchases({
      id: ticketPurchaseIds,
    })
    const qrCodeData: Record<string, string> = {}

    await promiseAll(
      ticketPurchases.map(async (ticketPurchase) => {
        qrCodeData[ticketPurchase.id] = await QRCode.toDataURL(
          ticketPurchase.id
        )
      })
    )

    return qrCodeData
  }
}
```

The `generateTicketQRCodes` method takes an array of ticket purchase IDs. It retrieves the ticket purchases and generates a QR code for each ticket purchase using the `qrcode` package.

The QR code encodes the ticket purchase ID, which can be used later for verification at the event entrance.

### c. Create Order Placed Subscriber

Next, you'll create a subscriber that listens to the `order.placed` event and sends an order confirmation email with the tickets.

A subscriber is an asynchronous function that is executed when its associated event is emitted.

To create a subscriber that sends a notification to the customer when an order is placed, create the file `src/subscribers/order-placed.ts` with the following content:

```ts title="src/subscribers/order-placed.ts"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/medusa"
import { TICKET_BOOKING_MODULE } from "../modules/ticket-booking"

export default async function handleOrderPlaced({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const query = container.resolve("query")
  const notificationModuleService = container.resolve("notification")
  const ticketBookingModuleService = container.resolve(TICKET_BOOKING_MODULE)

  const { data: [order] } = await query.graph({
    entity: "order",
    fields: [
      "id", 
      "email", 
      "created_at",
      "items.*",
      "ticket_purchases.*",
      "ticket_purchases.ticket_product.*",
      "ticket_purchases.ticket_product.product.*",
      "ticket_purchases.ticket_product.venue.*",
      "ticket_purchases.venue_row.*",
      "customer.*",
      "billing_address.*",
    ],
    filters: {
      id: data.id,
    },
  })

  const ticketPurchaseIds: string[] = order.ticket_purchases?.
    map((purchase) => purchase?.id).filter(Boolean) as string[] || []

  const qrCodes = await ticketBookingModuleService.generateTicketQRCodes(
    ticketPurchaseIds
  )
  const firstTicketPurchase = order.ticket_purchases?.[0]

  await notificationModuleService.createNotifications({
    to: order.email || "",
    channel: "feed",
    // TODO replace with a proper template
    template: "order.placed",
    data: {
      customer: {
        first_name: order.customer?.first_name || 
          order.billing_address?.first_name,
        last_name: order.customer?.last_name || 
          order.billing_address?.last_name,
      },
      order: {
        display_id: order.id,
        created_at: order.created_at,
        email: order.email,
      },
      show: {
        name: firstTicketPurchase?.ticket_product?.product?.title || 
          "Your Event",
        date: firstTicketPurchase?.show_date.toLocaleString(),
        venue: firstTicketPurchase?.ticket_product?.venue?.name || 
          "Venue Name",
      },
      tickets: order.ticket_purchases?.map((purchase) => ({
        label: purchase?.venue_row.row_type.toUpperCase(),
        seat: purchase?.seat_number,
        row: purchase?.venue_row.row_number,
        qr: qrCodes[purchase?.id || ""] || "",
      })),
      billing_address: order.billing_address,
    },
  })
}

export const config: SubscriberConfig = {
  event: "order.placed",
}
```

A subscriber file must export:

- An asynchronous function that is executed when its associated event is emitted.
- An object that indicates the event that the subscriber is listening to.

The subscriber receives among its parameters the data payload of the emitted event, which includes the order ID.

In the subscriber, you:

- Retrieve the order details, including ticket purchases, customer information, and billing address.
- Extract the ticket purchase IDs from the order.
- Generate QR codes for the ticket purchases.
- Use the Notification Module's service to send an email to the customer with the order details and tickets.
  - The `data` object contains the variables used in the email template you defined earlier.
  - Make sure to change the `template` property to match the template ID you created in your Notification Module Provider. For example, the ID of the dynamic template in SendGrid.

### Test Order Confirmation Email

To test the order confirmation email, [customize the storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/ticket-booking/example/storefront/index.html.md) first. Then, start the Medusa application and the Next.js Starter Storefront.

Place an order with at least one ticket product. After placing the order, you'll see the following message in your Medusa application's terminal:

```bash
info:    Processing order.placed which has 1 subscribers
```

If no errors occur, you should receive an order confirmation email at the email address you used when placing the order. The email should contain the order details and the tickets with QR codes.

### d. Verify Ticket QR Code Workflow

In this section, you'll implement functionality to verify QR codes, which is useful for checking tickets at event entrances.

To implement this functionality, you'll create a workflow and then execute it in an API route.

The workflow that verifies a ticket QR code has the following steps:

- [verifyTicketPurchaseStep](#verifyTicketPurchaseStep): Verifies a ticket purchase by its ID
- [updateTicketPurchaseStatusStep](#updateTicketPurchaseStatusStep): Updates the status of a ticket purchase

You need to implement both steps.

#### verifyTicketPurchaseStep

The `verifyTicketPurchaseStep` verifies that a ticket purchase is valid and has not been used yet.

To create the step, create the file `src/workflows/steps/verify-ticket-purchase-step.ts` with the following content:

```ts title="src/workflows/steps/verify-ticket-purchase-step.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { TICKET_BOOKING_MODULE } from "../../modules/ticket-booking"
import { MedusaError } from "@medusajs/framework/utils"

export type VerifyTicketPurchaseStepInput = {
  ticket_purchase_id: string
}

export const verifyTicketPurchaseStep = createStep(
  "verify-ticket-purchase",
  async (input: VerifyTicketPurchaseStepInput, { container }) => {
    const ticketBookingService = container.resolve(TICKET_BOOKING_MODULE)
    
    const ticketPurchase = await ticketBookingService.retrieveTicketPurchase(
      input.ticket_purchase_id
    )

    if (ticketPurchase.status !== "pending") {
      throw new MedusaError(
        MedusaError.Types.NOT_ALLOWED,
        "Ticket has already been scanned"
      )
    }

    if (ticketPurchase.show_date < new Date()) {
      throw new MedusaError(
        MedusaError.Types.NOT_ALLOWED,
        "Ticket is expired or show date has passed"
      )
    }

    return new StepResponse(true)
  }
)
```

The step takes the ticket purchase ID as input. In the step, you throw an error if the ticket has already been scanned or if the show date has passed.

#### updateTicketPurchaseStatusStep

The `updateTicketPurchaseStatusStep` updates the status of a ticket purchase.

To create the step, create the file `src/workflows/steps/update-ticket-purchase-status-step.ts` with the following content:

```ts title="src/workflows/steps/update-ticket-purchase-status-step.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { TICKET_BOOKING_MODULE } from "../../modules/ticket-booking"

export type UpdateTicketPurchaseStatusStepInput = {
  ticket_purchase_id: string
  status: "pending" | "scanned"
}

export const updateTicketPurchaseStatusStep = createStep(
  "update-ticket-purchase-status",
  async (input: UpdateTicketPurchaseStatusStepInput, { container }) => {
    const ticketBookingService = container.resolve(TICKET_BOOKING_MODULE)
    
    const currentTicket = await ticketBookingService.retrieveTicketPurchase(
      input.ticket_purchase_id
    )
    
    const updatedTicket = await ticketBookingService.updateTicketPurchases({
      id: input.ticket_purchase_id,
      status: input.status,
    })

    return new StepResponse(updatedTicket, {
      id: input.ticket_purchase_id,
      previousStatus: currentTicket.status,
    })
  },
  async (compensationData, { container }) => {
    if (!compensationData) {return}
    
    const ticketBookingService = container.resolve(TICKET_BOOKING_MODULE)
    await ticketBookingService.updateTicketPurchases({
      id: compensationData.id,
      status: compensationData.previousStatus,
    })
  }
)
```

The step receives the ticket purchase ID and the new status as input.

In the step, you update the ticket purchase status. In the compensation function, you undo the update if an error occurs in the workflow.

#### Verify Ticket QR Code Workflow

You can now create the workflow that verifies a ticket QR code.

Create the file `src/workflows/verify-ticket-purchase.ts` with the following content:

```ts title="src/workflows/verify-ticket-purchase.ts"
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { verifyTicketPurchaseStep } from "./steps/verify-ticket-purchase-step"
import { updateTicketPurchaseStatusStep } from "./steps/update-ticket-purchase-status-step"

export type VerifyTicketPurchaseWorkflowInput = {
  ticket_purchase_id: string
}

export const verifyTicketPurchaseWorkflow = createWorkflow(
  "verify-ticket-purchase",
  function (input: VerifyTicketPurchaseWorkflowInput) {
    verifyTicketPurchaseStep(input)
    
    const ticketPurchase = updateTicketPurchaseStatusStep({
      ticket_purchase_id: input.ticket_purchase_id,
      status: "scanned",
    })

    return new WorkflowResponse(ticketPurchase)
  }
)
```

The workflow receives the ticket purchase ID as input.

In the workflow, you verify the ticket purchase and then update its status to `scanned`.

### e. Verify Ticket API Route

Finally, you'll create an API route that executes the `verifyTicketPurchaseWorkflow` workflow to verify a ticket QR code. A QR scanner can call this API route when scanning a ticket at the event entrance.

To create the API route, create the file `src/api/tickets/[id]/verify/route.ts` with the following content:

```ts title="src/api/tickets/[id]/verify/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { verifyTicketPurchaseWorkflow } from "../../../../workflows/verify-ticket-purchase"

export async function POST(req: MedusaRequest, res: MedusaResponse) {
  const { id } = req.params

  await verifyTicketPurchaseWorkflow(req.scope).run({
    input: {
      ticket_purchase_id: id,
    },
  })
  
  res.json({
    success: true,
  })
}
```

You expose a `POST` API route at `/tickets/:id/verify`. In the route handler, you execute the `verifyTicketPurchaseWorkflow` workflow with the ticket purchase ID from the URL parameters.

If the ticket is valid, the route returns a success response with `200` status code. If the ticket is invalid, an error is thrown with `400` status code.

### Test Ticket Verification

To test the ticket verification functionality, start the Medusa application.

Then, place an order with at least one ticket product. After placing the order, check your email for the order confirmation email containing the tickets with QR codes.

Next, decode the QR code to get the ticket purchase ID. You can use an online QR code decoder or a QR code scanner app on your phone.

Finally, make a `POST` request to the `/tickets/:id/verify` API route with the ticket purchase ID:

```bash
curl -X POST http://localhost:9000/tickets/{ticket_purchase_id}/verify
```

Replace `{ticket_purchase_id}` with the actual ticket purchase ID you obtained from the QR code.

If the ticket is valid, you'll receive a success response. Otherwise, you'll receive an error response indicating the ticket is invalid or has already been scanned.

***

## Next Steps

You've now implemented the ticket booking functionality in the backend. Follow the [Ticket Booking Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/ticket-booking/example/storefront/index.html.md) tutorial to customize the Next.js Starter Storefront for ticket booking.

You can expand on this tutorial by adding more features, such as:

- Provide more management features for venues and ticket products, such as updating and deleting them.
- Allow purchasing tickets for specific times in a day.
- Handle [order events](https://docs.medusajs.com/references/events#order-events/index.html.md), such as `order.canceled`, to make changes to ticket purchases.

### Learn More about Medusa

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.


# Customize Storefront for Ticket Booking

In this tutorial, you'll customize the Next.js Starter Storefront based on the [Ticket Booking system](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/ticket-booking/example/index.html.md) you implemented in the backend.

### Prerequisites

- [Medusa backend with ticket booking functionality](../page.mdx)
- [Next.js Starter Storefront](../../../..//nextjs-starter/page.mdx)

## Summary

In this tutorial, you'll customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to support ticket booking:

1. **Product Page**: Allow customers to choose a show date, select seats from a seating layout, and add tickets to the cart.
2. **Cart Page**: Display selected tickets with their seat numbers and show dates, and remove quantity controls for ticket items.
3. **Checkout Page**: Remove the shipping steps and complete the cart using the custom complete cart API route you created earlier.
4. **Order Confirmation Page**: Display ticket details for ticket items.

![Preview of the seat selection on the product page](https://res.cloudinary.com/dza7lstvk/image/upload/v1757422831/Medusa%20Resources/CleanShot_2025-09-09_at_15.59.33_2x_afkysa.png)

If you installed the Next.js Starter Storefront with the Medusa backend, the storefront was installed in a separate directory. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-ticketing`, you can find the storefront by going back to the parent directory and changing to the `medusa-ticketing-storefront` directory:

```bash
cd ../medusa-ticketing-storefront # change based on your project name
```

## Step 1: Ticket Product Utilities

Before you customize the storefront, you'll create some types and utility functions that you'll use in your customizations.

Create the file `src/lib/util/ticket-product.ts` with the following content:

```ts title="src/lib/util/ticket-product.ts"
import { HttpTypes } from "@medusajs/types"

export interface TicketProductAvailability {
  date: string
  row_types: {
    row_type: string
    total_seats: number
    available_seats: number
    sold_out: boolean
  }[]
  sold_out: boolean
}

export interface TicketProduct {
  id: string
  product_id: string
  venue: {
    id: string
    name: string
    address: string
    rows: Array<{
      id: string
      row_number: string
      row_type: string
      seat_count: number
    }>
  }
  dates: string[]
}

export interface TicketProductAvailabilityData {
  ticket_product: TicketProduct
  availability: TicketProductAvailability[]
}

export interface TicketProductSeatsData {
  venue: {
    id: string
    name: string
    address: string
    rows: Array<{
      id: string
      row_number: string
      row_type: string
      seat_count: number
    }>
  }
  date: string
  seat_map: {
    row_number: string
    row_type: string
    seats: {
      number: string
      is_purchased: boolean
      variant_id: string | null
    }[]
  }[]
}

/**
 - Check if a product is a ticket product by looking for the ticket_product property
 */
export function isTicketProduct(product: HttpTypes.StoreProduct): boolean {
  return !!(product as any).ticket_product
}
```

You define types for the ticket product availability and seating layout API responses, as well as a utility function to check if a product is a ticket product.

***

## Step 2: Support Adding Items to Cart with Metadata

Next, you'll add support for adding items to the cart with metadata. This is necessary because when adding tickets to the cart, you need to include metadata such as the seat number and show date.

This functionality is supported by the Medusa APIs, but you need to update the `addToCart` function in the storefront to accept and send metadata.

In `src/lib/data/cart.ts`, find the `addToCart` function and update its object parameter to include an optional `metadata` property:

```ts title="src/lib/data/cart.ts" highlights={[["3"], ["6"]]}
export async function addToCart({
  // ...
  metadata,
}: {
  // ...
  metadata?: Record<string, any>
}) {
  // ...
}
```

Then, find the `sdk.store.cart.createLineItem` method usage and pass the `metadata` property to it:

```ts title="src/lib/data/cart.ts" highlights={[["6"]]}
await sdk.store.cart
  .createLineItem(
    cart.id,
    {
      // ...
      metadata,
    }
    // ...
  )
```

Now, the `addToCart` function accepts an optional `metadata` property and sends it to the Medusa backend when creating a line item.

***

## Step 3: Customize Product Page

Next, you'll customize the product page. This includes:

- Creating a component that displays a calendar to choose a show date, with available dates determined by the availability API route you created earlier.
- Creating a component that displays a seat chart in a modal to select seats, with purchased seats determined by the seating layout API route you created earlier.
- Customizing the product page to use the above components.

### Date Selector Component

The date selector component will fetch the available dates for a ticket product using the API route you created in the [backend part of the tutorial](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/ticket-booking/example#a-available-dates-api-route/index.html.md). Before creating the component, you'll add a server function to fetch the available dates.

Create the file `src/lib/data/ticket-products.ts` with the following content:

```ts title="src/lib/data/ticket-products.ts"
"use server"

import { sdk } from "@lib/config"
import { getAuthHeaders, getCacheOptions } from "./cookies"
import { TicketProductAvailabilityData } from "@lib/util/ticket-product"

export const getTicketProductAvailability = async (
  productId: string
): Promise<TicketProductAvailabilityData> => {
  const headers = {
    ...(await getAuthHeaders()),
  }

  const next = {
    ...(await getCacheOptions("ticket-products")),
  }

  return sdk.client
    .fetch<TicketProductAvailabilityData>(
      `/store/ticket-products/${productId}/availability`,
      {
        method: "GET",
        headers,
        next,
        cache: "no-store", // Always fetch fresh data for availability
      }
    )
    .then((data) => data)
}
```

You create a server function `getTicketProductAvailability` that fetches the available dates for a ticket product using the `/store/ticket-products/{id}/availability` API route.

Next, you'll create the date selector component. It will show an input to select the number of tickets and a calendar to select a date.

![Preview of the date selector component](https://res.cloudinary.com/dza7lstvk/image/upload/v1757495634/Medusa%20Resources/CleanShot_2025-09-10_at_12.13.25_2x_adyjpu.png)

Create the file `src/modules/products/components/ticket-product-layout/date-quantity-selection.tsx` with the following content:

```tsx title="src/modules/products/components/ticket-product-layout/date-quantity-selection.tsx" collapsibleLines="1-9" expandButtonLabel="Show Imports"
"use client"

import { getTicketProductAvailability } from "@lib/data/ticket-products"
import { TicketProductAvailability } from "@lib/util/ticket-product"
import { HttpTypes } from "@medusajs/types"
import { Button, Calendar, Label, IconButton, toast } from "@medusajs/ui"
import { Minus, Plus } from "@medusajs/icons"
import { useState, useEffect } from "react"

type TicketDateSelectionProps = {
  product: HttpTypes.StoreProduct
  onDateSelect: (date: string, nbOfTickets: number) => void
  disabled?: boolean
}

export default function TicketDateSelection({
  product,
  onDateSelect,
  disabled = false,
}: TicketDateSelectionProps) {
  const [nbOfTickets, setNbOfTickets] = useState(1)
  const [selectedDate, setSelectedDate] = useState<Date | null>(null)
  const [availability, setAvailability] = useState<TicketProductAvailability[]>([])
  const [isLoading, setIsLoading] = useState(false)

  // Load availability data on mount
  useEffect(() => {
    const loadAvailability = async () => {
      setIsLoading(true)
      try {
        const data = await getTicketProductAvailability(product.id)
        setAvailability(data.availability)
      } catch (error) {
        toast.error("Failed to load ticket availability: " + error)
      } finally {
        setIsLoading(false)
      }
    }

    loadAvailability()
  }, [product.id])

  const getTotalAvailableSeats = (dateAvailability: TicketProductAvailability) => {
    return dateAvailability.row_types.reduce(
      (sum, rowType) => sum + rowType.available_seats, 0
    )
  }

  const getFilteredAvailability = (quantity: number) => {
    return availability.filter((avail) => getTotalAvailableSeats(avail) >= quantity)
  }

  const filteredAvailability = getFilteredAvailability(nbOfTickets)

  const dateAsStr = (date: Date) => {
    return `${
      date.getFullYear()
    }-${
      String(date.getMonth() + 1).padStart(2, "0")
    }-${String(date.getDate()).padStart(2, "0")}`
  }

  const isDateUnavailable = (date: Date) => {
    const dateString = dateAsStr(date)
    return !filteredAvailability.some((avail) => avail.date === dateString)
  }

  const handleDateChange = (date: Date | null) => {
    setSelectedDate(date)
  }

  const handlePickSeats = () => {
    if (!selectedDate) {
      toast.error("Please select a date")
      return
    }
    
    const dateString = dateAsStr(selectedDate)
    onDateSelect(dateString, nbOfTickets)
  }

  // TODO render date selector
}
```

The `TicketDateSelection` component accepts the following props:

- `product`: The Medusa product to select a date for.
- `onDateSelect`: A callback function that is called when the date and number of tickets are selected.
- `disabled`: A boolean to disable the component.

In the component, you define state variables to manage the number of tickets, the selected date, the availability data, and the loading state. You also load the availability data when the component mounts.

In addition, you define the following functions:

- `getTotalAvailableSeats`: Calculates the total available seats for a given date availability.
- `getFilteredAvailability`: Filters the availability data based on the number of tickets requested.
- `dateAsStr`: Converts a `Date` object to a string in the format `YYYY-MM-DD`.
- `isDateUnavailable`: Checks if a date is unavailable based on the filtered availability.
- `handleDateChange`: Updates the selected date when the user selects a date from the calendar.
- `handlePickSeats`: Calls the `onDateSelect` callback with the selected date and number of tickets, or shows an error if no date is selected.

Next, you'll render the date selector form. Replace the `TODO` with the following:

```tsx title="src/modules/products/components/ticket-product-layout/date-quantity-selection.tsx"
return (
  <div className="bg-ui-bg-base">
    <h3 className="txt-large text-center mb-4">Select Show Date</h3>
    {isLoading && (
      <div className="flex flex-col gap-y-4">
        <div className="h-6 bg-ui-bg-subtle rounded animate-pulse w-32 mx-auto" />
        <div className="h-10 bg-ui-bg-subtle rounded animate-pulse w-48 mx-auto" />
        <div className="h-6 bg-ui-bg-subtle rounded animate-pulse w-40 mx-auto" />
      </div>
    )}
    {!isLoading && (
      <div className="flex flex-col gap-y-4">
        {/* Number of tickets selection */}
        <div className="flex justify-center">
          <div className="flex flex-col gap-y-2">
            <Label htmlFor="nbOfTickets" className="text-ui-fg-subtle txt-compact-small">Number of Tickets</Label>
            <div className="flex items-center justify-between rounded-md">
              <IconButton
                onClick={() => setNbOfTickets(Math.max(1, nbOfTickets - 1))}
                disabled={disabled || nbOfTickets <= 1}
                variant="transparent"
              >
                <Minus />
              </IconButton>
              {nbOfTickets}
              <IconButton
                onClick={() => setNbOfTickets(Math.min(10, nbOfTickets + 1))}
                disabled={disabled || nbOfTickets >= 10}
                variant="transparent"
              >
                <Plus />
              </IconButton>
            </div>
          </div>
        </div>

        {/* Calendar */}
        <div className="flex justify-center">
          <Calendar
            onChange={handleDateChange}
            minValue={filteredAvailability.length > 0 ? 
              new Date(filteredAvailability[0].date) : undefined
            }
            maxValue={filteredAvailability.length > 0 ? 
              new Date(filteredAvailability[filteredAvailability.length - 1].date) : undefined
            }
            isDateUnavailable={isDateUnavailable}
          />
        </div>

        {/* Available dates info */}
        {filteredAvailability.length > 0 && (
          <p className="txt-small text-ui-fg-subtle text-center txt-compact-small">
            {filteredAvailability.length} show{filteredAvailability.length !== 1 ? "s" : ""} available for {nbOfTickets} ticket{nbOfTickets !== 1 ? "s" : ""}
          </p>
        )}

        {/* Pick Seats Button */}
        <Button
          onClick={handlePickSeats}
          disabled={disabled || !selectedDate || filteredAvailability.length === 0}
          variant="primary"
          className="w-full"
        >
          Pick Seats
        </Button>
      </div>
    )}
  </div>
)
```

You display a quantity selector to choose the number of tickets, which affects the available dates shown in the calendar based on whether they have enough available seats.

You also display a calendar to select a show date, and a button to pick seats that calls the `onDateSelect` callback with the selected date and number of tickets.

### Seat Selector Component

The seat selector component will fetch the seating layout for a venue and date using the API route you created in the [backend part of the tutorial](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/ticket-booking/example#b-seating-layout-api-route/index.html.md). Before creating the component, you'll add a server function to fetch the seating layout.

In `src/lib/data/ticket-products.ts`, add the following import at the top of the file:

```ts title="src/lib/data/ticket-products.ts"
import { TicketProductSeatsData } from "@lib/util/ticket-product"
```

Then, add the following function to the file:

```ts title="src/lib/data/ticket-products.ts"
export const getTicketProductSeats = async (
  productId: string,
  date: string
): Promise<TicketProductSeatsData> => {
  const headers = {
    ...(await getAuthHeaders()),
  }

  const next = {
    ...(await getCacheOptions("ticket-products")),
  }

  return sdk.client
    .fetch<TicketProductSeatsData>(
      `/store/ticket-products/${productId}/seats`,
      {
        method: "GET",
        query: {
          date,
        },
        headers,
        next,
        cache: "no-store", // Always fetch fresh data for seats
      }
    )
    .then((data) => data)
}
```

You create a server function `getTicketProductSeats` that fetches the seating layout for a ticket product and date using the `/store/ticket-products/{id}/seats` API route.

Next, you'll create the seat selector component. It will show a modal with a seat chart to select seats, and a button to add the seats to the cart and proceed to checkout.

![Preview of the seat selector component](https://res.cloudinary.com/dza7lstvk/image/upload/v1757496623/Medusa%20Resources/CleanShot_2025-09-10_at_12.30.07_2x_ylhhk1.png)

You'll first create the seat chart component that displays the seating layout and allows selecting seats. Create the file `src/modules/products/components/ticket-product-layout/seat-selection.tsx` with the following content:

```tsx title="src/modules/products/components/ticket-product-layout/seat-selection.tsx" collapsibleLines="1-7" expandButtonLabel="Show Imports"
"use client"

import { TicketProductSeatsData } from "@lib/util/ticket-product"
import { Tooltip, TooltipProvider } from "@medusajs/ui"
import { HttpTypes } from "@medusajs/types"
import { convertToLocale } from "../../../../lib/util/money"

export type SelectedSeat = {
  seatNumber: string
  rowNumber: string
  rowType: string
  variantId: string
  date: string
  venueRowId: string
}

type SeatSelectionProps = {
  seatData: TicketProductSeatsData
  selectedSeats: SelectedSeat[]
  onSeatSelect: (seat: SelectedSeat) => void
  disabled?: boolean
  product: HttpTypes.StoreProduct
  maxSeats: number
}

export default function SeatSelection({
  seatData,
  selectedSeats,
  onSeatSelect,
  disabled = false,
  product,
  maxSeats,
}: SeatSelectionProps) {
  const getSeatStatus = (rowNumber: string, seatNumber: string) => {
    const seat = seatData.seat_map
      .find((row) => row.row_number === rowNumber)
      ?.seats.find((s) => s.number === seatNumber)

    if (!seat) {return "unavailable"}

    if (seat.is_purchased) {return "purchased"}
    
    const isSelected = selectedSeats.some((s) => 
      s.seatNumber === seatNumber && s.rowNumber === rowNumber && s.date === seatData.date
    )
    
    if (isSelected) {return "selected"}
    
    return "available"
  }

  const getSeatColor = (status: string) => {
    switch (status) {
      case "purchased":
        return "bg-ui-tag-neutral-bg text-ui-tag-neutral-text cursor-not-allowed"
      case "selected":
        return "bg-ui-tag-blue-bg text-ui-tag-blue-text"
      case "available":
        return "bg-ui-tag-green-bg text-ui-tag-green-text cursor-pointer hover:bg-ui-tag-green-bg-hover"
      default:
        return "bg-ui-tag-neutral-bg text-ui-tag-neutral-text cursor-not-allowed"
    }
  }

  const formatRowType = (rowType: string) => {
    switch (rowType.toLowerCase()) {
      case "vip":
        return "VIP"
      default:
        return rowType.charAt(0).toUpperCase() + rowType.slice(1).toLowerCase()
    }
  }

  // TODO handle seat selection
}
```

The `SeatSelection` component accepts the following props:

- `seatData`: The seating layout data for the venue and date retrieved from the API.
- `selectedSeats`: An array of currently selected seats.
- `onSeatSelect`: A callback function that is called when a seat is selected.
- `disabled`: A boolean to disable seat selection.
- `product`: The Medusa product to select seats for.
- `maxSeats`: The maximum number of seats that can be selected.

In the component, you define the following functions:

- `getSeatStatus`: Determines the status of a seat (purchased, selected, available, or unavailable) based on the seating data and selected seats.
- `getSeatColor`: Returns the appropriate CSS classes for a seat based on its status.
- `formatRowType`: Formats the row type string for display.

Next, you'll add a function to handle seat selection. Replace the `TODO` with the following:

```tsx title="src/modules/products/components/ticket-product-layout/seat-selection.tsx"
const handleSeatClick = (rowNumber: string, seatNumber: string, rowType: string) => {
  if (disabled) {return}

  const seat = seatData.seat_map
    .find((row) => row.row_number === rowNumber)
    ?.seats.find((s) => s.number === seatNumber)

  if (!seat || seat.is_purchased || !seat.variant_id) {return}

  // Check if seat is already selected
  const isAlreadySelected = selectedSeats.some(
    (selectedSeat) => selectedSeat.seatNumber === seatNumber && selectedSeat.rowNumber === rowNumber
  )

  if (isAlreadySelected) {
    // Unselect the seat
    onSeatSelect({
      seatNumber,
      rowNumber,
      rowType,
      variantId: seat.variant_id,
      date: seatData.date,
      venueRowId: seatData.venue.rows.find((row) => row.row_number === rowNumber)?.id as string,
    })
    return
  }

  // Check if we've reached the maximum number of seats
  if (selectedSeats.length >= maxSeats) {
    return
  }

  // Select the seat
  onSeatSelect({
    seatNumber,
    rowNumber,
    rowType,
    variantId: seat.variant_id,
    date: seatData.date,
    venueRowId: seatData.venue.rows.find((row) => row.row_number === rowNumber)?.id as string,
  })
}

// TODO render seat chart
```

You define the `handleSeatClick` function that handles the logic for toggling seat selection. It checks if the seat is purchasable, if it's already selected, and if the maximum number of seats has been reached before calling the `onSeatSelect` callback.

Finally, you'll render the seat chart. Replace the `TODO` with the following:

```tsx title="src/modules/products/components/ticket-product-layout/seat-selection.tsx"
return (
  <TooltipProvider>
    <div className="flex flex-col gap-y-4">
      {/* Theater Layout */}
      <div className="bg-gradient-to-b from-ui-bg-subtle to-ui-bg-subtle rounded-lg p-4 shadow-elevation-card-rest overflow-y-auto max-h-[500px]">
        {/* Stage Area */}
        <div className="text-center mb-6">
          <div className="bg-gradient-to-b from-ui-fg-base to-ui-fg-base text-white px-8 py-4 rounded-lg shadow-xl relative">
            <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent rounded-lg"></div>
            <div className="relative">
              <div className="txt-large-plus mb-1">STAGE</div>
              <div className="txt-small text-ui-alpha-250">Performance Area</div>
            </div>
          </div>
        </div>

        {/* Seating Area */}
        <div>
          <div className="space-y-4">
            {seatData.seat_map.map((row, index) => (
              <div key={row.row_number} className="flex items-baseline justify-center gap-x-3">
                {/* Row label */}
                <div className="txt-small text-ui-fg-subtle">
                  {row.row_number}
                </div>

                {/* Seats */}
                <div className="flex gap-x-1 gap-y-1 flex-wrap justify-center">
                  {row.seats.map((seat) => {
                    const status = getSeatStatus(row.row_number, seat.number)
                    const variant = product.variants?.find((v) => v.id === seat.variant_id)
                    const seatPrice = variant?.calculated_price?.calculated_amount || 0
                    const currencyCode = variant?.calculated_price?.currency_code || "USD"
                    
                    const tooltipContent = status === "purchased" 
                      ? "Sold" 
                      : status === "selected"
                      ? "Selected"
                      : seatPrice > 0
                      ? `Seat ${seat.number} - ${formatRowType(row.row_type)} - ${convertToLocale({
                          amount: seatPrice,
                          currency_code: currencyCode,
                          minimumFractionDigits: 0,
                        })}`
                      : `Seat ${seat.number} - ${formatRowType(row.row_type)}`

                    return (
                      <Tooltip key={seat.number} content={tooltipContent} className="z-[76]">
                        <button
                          onClick={() => handleSeatClick(row.row_number, seat.number, row.row_type)}
                          disabled={disabled || status === "purchased" || status === "unavailable" || (status === "available" && selectedSeats.length >= maxSeats)}
                          className={`
                            w-8 h-8 rounded-sm txt-xsmall transition-all duration-200 flex items-center justify-center
                            ${getSeatColor(status)}
                            ${status === "purchased" ? "cursor-not-allowed" : "cursor-pointer"}
                            ${(status === "available" && selectedSeats.length >= maxSeats) || status === "purchased" ? "opacity-50 cursor-not-allowed" : ""}
                            ${status === "available" ? "shadow-sm" : ""}
                            ${status === "selected" ? "border border-ui-border-interactive" : ""}
                          `}
                        >
                          {seat.number}
                        </button>
                      </Tooltip>
                    )
                  })}
                </div>
              </div>
            ))}
          </div>
        </div>
      </div>

      {/* Legend */}
      <div className="flex flex-wrap justify-center gap-x-6 gap-y-2 txt-small">
        <div className="flex items-center gap-x-2">
          <div className="w-5 h-5 bg-ui-tag-green-bg rounded"></div>
          <span className="txt-small-plus">Available</span>
        </div>
        <div className="flex items-center gap-x-2">
          <div className="w-5 h-5 bg-ui-tag-blue-bg rounded"></div>
          <span className="txt-small-plus">Selected</span>
        </div>
        <div className="flex items-center gap-x-2">
          <div className="w-5 h-5 bg-ui-tag-neutral-bg rounded"></div>
          <span className="txt-small-plus">Sold</span>
        </div>
      </div>
    </div>
  </TooltipProvider>
)
```

You display the seating layout with rows and seats. Each seat is a button that shows its status (available, selected, purchased) with different colors and tooltips. You also include a legend to explain the seat colors.

Next, you'll create the seat selector modal component that renders the `SeatSelection` component inside a modal. Create the file `src/modules/products/components/ticket-product-layout/seat-selection-modal.tsx` with the following content:

```tsx title="src/modules/products/components/ticket-product-layout/seat-selection-modal.tsx" collapsibleLines="1-13" expandButtonLabel="Show Imports"
"use client"

import { addToCart } from "@lib/data/cart"
import { getTicketProductSeats } from "@lib/data/ticket-products"
import { TicketProductSeatsData } from "@lib/util/ticket-product"
import { HttpTypes } from "@medusajs/types"
import { Button, toast } from "@medusajs/ui"
import { useParams, useRouter } from "next/navigation"
import { useEffect, useMemo, useState } from "react"
import { convertToLocale } from "@lib/util/money"
import SeatSelection, { SelectedSeat } from "./seat-selection"
import Modal from "../../../common/components/modal"

type SeatSelectionModalProps = {
  product: HttpTypes.StoreProduct
  selectedDate: string
  selectedQuantity: number
  isOpen: boolean
  onClose: () => void
  disabled?: boolean
}

export default function SeatSelectionModal({
  product,
  selectedDate,
  selectedQuantity,
  isOpen,
  onClose,
  disabled = false,
}: SeatSelectionModalProps) {
  const [selectedSeats, setSelectedSeats] = useState<SelectedSeat[]>([])
  const [seatData, setSeatData] = useState<TicketProductSeatsData | null>(null)
  const [isAddingToCart, setIsAddingToCart] = useState(false)
  
  const countryCode = useParams().countryCode as string
  const router = useRouter()

  const formatDate = (dateString: string) => {
    const date = new Date(dateString)
    return date.toLocaleDateString("en-US", {
      weekday: "long",
      year: "numeric",
      month: "long",
      day: "numeric",
    })
  }

  const totalPrice = useMemo(() => {
    return selectedSeats.reduce((total, seat) => {
      const variant = product.variants?.find((v) => v.id === seat.variantId)
      return total + (variant?.calculated_price?.calculated_amount || 0)
    }, 0)
  }, [selectedSeats, product.variants])

  // Load seat data when modal opens
  useEffect(() => {
    if (!isOpen || !selectedDate) {
      return
    }
    setSelectedSeats([])
    setSeatData(null)

    const loadSeatData = async () => {
      try {
        const data = await getTicketProductSeats(product.id, selectedDate)
        setSeatData(data)
      } catch (error) {
        toast.error("Failed to load seat data: " + error)
      }
    }

    loadSeatData()
  }, [isOpen, selectedDate, product.id])

  // TODO handle seat selection and cart addition
}
```

The `SeatSelectionModal` component accepts the following props:

- `product`: The Medusa product to select seats for.
- `selectedDate`: The selected show date.
- `selectedQuantity`: The number of tickets to select.
- `isOpen`: A boolean to control the visibility of the modal.
- `onClose`: A callback function that is called when the modal is closed.
- `disabled`: A boolean to disable seat selection.

In the component, you define state and memo variables to manage the selected seats, the seating data, the loading state for the cart addition action, and the total price for the seat selection. You also load the seating data when the modal opens.

Next, you'll add functions to handle seat selection and adding seats to the cart. Replace the `TODO` with the following:

```tsx title="src/modules/products/components/ticket-product-layout/seat-selection-modal.tsx"
const handleSeatSelect = (seat: SelectedSeat) => {
  setSelectedSeats((prev) => {
    const existingIndex = prev.findIndex(
      (s) => s.seatNumber === seat.seatNumber && s.rowNumber === seat.rowNumber
    )
    
    if (existingIndex >= 0) {
      // Remove seat if already selected
      return prev.filter((_, index) => index !== existingIndex)
    } else {
      // Add seat if not selected and under limit
      if (prev.length < selectedQuantity) {
        return [...prev, seat]
      }
      return prev
    }
  })
}

const handleAddToCart = async () => {
  if (selectedSeats.length === 0) {
    toast.error("Please select at least one seat")
    return
  }

  if (selectedSeats.length !== selectedQuantity) {
    toast.error(`Please select exactly ${selectedQuantity} seat${selectedQuantity !== 1 ? "s" : ""}`)
    return
  }

  setIsAddingToCart(true)
  try {
    // Add each seat as a separate cart item
    for (const seat of selectedSeats) {
      await addToCart({
        variantId: seat.variantId,
        quantity: 1,
        countryCode,
        metadata: {
          seat_number: seat.seatNumber,
          row_number: seat.rowNumber,
          show_date: seat.date,
          venue_row_id: seat.venueRowId,
        },
      })
    }

    toast.success(`Added ${selectedSeats.length} ticket${selectedSeats.length !== 1 ? "s" : ""} to cart`)
    
    // Redirect to checkout
    router.push(`/${countryCode}/checkout?step=address`)
  } catch (error) {
    toast.error("Failed to add tickets to cart: " + error)
  } finally {
    setIsAddingToCart(false)
  }
}

// TODO render modal with seat selection
```

You define the `handleSeatSelect` function that updates the selected seats when a seat is clicked, and the `handleAddToCart` function that adds the selected seats to the cart and redirects to the checkout page.

Finally, you'll render the modal with the seat selection component. Replace the `TODO` with the following:

```tsx title="src/modules/products/components/ticket-product-layout/seat-selection-modal.tsx"
return (
  <Modal isOpen={isOpen} close={onClose}>
    <div className="flex items-center justify-between w-full mb-4">
      <div>
        <h2 className="txt-large-plus">Select Your Seats</h2>
        <p className="txt-small text-ui-fg-subtle">
          {formatDate(selectedDate)} • {seatData?.venue.name} • ({selectedSeats.length}/{selectedQuantity} tickets selected)
        </p>
      </div>
      <Button
        variant="transparent"
        onClick={onClose}
        className="text-ui-fg-muted hover:text-ui-fg-base"
      >
        ✕
      </Button>
    </div>
    
    <div>
      {seatData ? (
        <SeatSelection
          product={product}
          seatData={seatData}
          selectedSeats={selectedSeats}
          onSeatSelect={handleSeatSelect}
          maxSeats={selectedQuantity}
          disabled={disabled || isAddingToCart}
        />
      ) : (
        <div className="text-center py-8 min-h-[500px]">
          <p className="txt-medium text-ui-fg-subtle">No seat data available</p>
        </div>
      )}
    </div>

    {seatData && (
      <div className="flex items-center justify-between w-full mt-4">
        <div className="flex items-center justify-between w-full">
          <div className="txt-small text-ui-fg-subtle">
            {selectedSeats.length} of {selectedQuantity} seats selected
          </div>
          <div className="flex gap-x-3">
            <Button
              variant="secondary"
              onClick={onClose}
              disabled={isAddingToCart}
            >
              Cancel
            </Button>
            <Button
              variant="primary"
              onClick={handleAddToCart}
              disabled={disabled || isAddingToCart || selectedSeats.length !== selectedQuantity}
              isLoading={isAddingToCart}
            >
              {selectedSeats.length > 0 ? (
                <>
                  Buy Tickets - {convertToLocale({
                    amount: totalPrice,
                    currency_code: product.variants?.[0]?.calculated_price?.currency_code || "USD",
                  })}
                </>
              ) : (
                "Buy Tickets"
              )}
            </Button>
          </div>
        </div>
      </div>
    )}
  </Modal>
)
```

You display a modal with the seat selection component, along with a header and a footer. You use the `Modal` component that's part of the Next.js Starter Storefront.

### Add Ticket Layout Component

Next, you'll create a component that renders the date selector and seat selector modal components together. You'll display this component on the product page for ticket products.

Create the file `src/modules/products/components/ticket-product-layout/index.tsx` with the following content:

```tsx title="src/modules/products/components/ticket-product-layout/index.tsx"
"use client"

import React, { useState } from "react"
import { HttpTypes } from "@medusajs/types"
import TicketDateSelection from "./date-quantity-selection"
import SeatSelectionModal from "./seat-selection-modal"

type TicketLayoutProps = {
  product: HttpTypes.StoreProduct
}

const TicketLayout: React.FC<TicketLayoutProps> = ({ product }) => {
  const [isSeatModalOpen, setIsSeatModalOpen] = useState(false)
  const [selectedDate, setSelectedDate] = useState<string | null>(null)
  const [selectedQuantity, setSelectedQuantity] = useState<number>(1)

  const handleDateQuantitySelect = (date: string, quantity: number) => {
    setSelectedDate(date)
    setSelectedQuantity(quantity)
    setIsSeatModalOpen(true)
  }

  const handleCloseSeatModal = () => {
    setIsSeatModalOpen(false)
    setSelectedDate(null)
    setSelectedQuantity(1)
  }

  return (
    <>
      <TicketDateSelection
        product={product}
        onDateSelect={handleDateQuantitySelect}
        disabled={false}
      />
      
      <SeatSelectionModal
        product={product}
        selectedDate={selectedDate || ""}
        selectedQuantity={selectedQuantity}
        isOpen={isSeatModalOpen}
        onClose={handleCloseSeatModal}
        disabled={false}
      />
    </>
  )
}

export default TicketLayout
```

The `TicketLayout` component manages the state for the selected date, number of tickets, and the visibility of the seat selection modal. It renders the `TicketDateSelection` component to select a date and quantity, and the `SeatSelectionModal` component to select seats.

When a date and quantity are selected, it opens the seat selection modal. When the modal is closed, it resets the state.

### Update Product Page Layout

Next, you'll update the product page layout to use the date selector and seat selector modal components you created earlier.

In `src/modules/products/templates/index.tsx`, replace the file content with the following:

```tsx title="src/modules/products/templates/index.tsx" collapsibleLines="1-10" expandButtonLabel="Show Imports"
import React, { Suspense } from "react"

import ImageGallery from "@modules/products/components/image-gallery"
import RelatedProducts from "@modules/products/components/related-products"
import ProductInfo from "@modules/products/templates/product-info"
import SkeletonRelatedProducts from "@modules/skeletons/templates/skeleton-related-products"
import { notFound } from "next/navigation"
import { HttpTypes } from "@medusajs/types"
import TicketLayout from "../components/ticket-product-layout"

type ProductTemplateProps = {
  product: HttpTypes.StoreProduct
  region: HttpTypes.StoreRegion
  countryCode: string
}

const ProductTemplate: React.FC<ProductTemplateProps> = ({
  product,
  countryCode,
}) => {
  if (!product || !product.id) {
    return notFound()
  }

  return (
    <>
      <div
        className="content-container flex flex-col py-6 relative"
        data-testid="product-container"
      >
        <div className="flex flex-col small:flex-row small:items-start gap-y-6">
          <div className="flex flex-col small:sticky small:top-48 small:py-0 small:max-w-[300px] w-full py-8 gap-y-6">
            <ProductInfo product={product} />
          </div>
          <div className="flex flex-col w-full relative">
            <div className="block w-full relative">
              <ImageGallery images={product?.images || []} />
            </div>
          </div>
          <div className="flex flex-col small:sticky small:top-48 small:py-0 small:max-w-[300px] w-full py-8 gap-y-6">
            <TicketLayout product={product} />
          </div>
        </div>
      </div>
      
      <div
        className="content-container my-16 small:my-32"
        data-testid="related-products-container"
      >
        <Suspense fallback={<SkeletonRelatedProducts />}>
          <RelatedProducts product={product} countryCode={countryCode} />
        </Suspense>
      </div>
    </>
  )
}

export default ProductTemplate
```

This makes the following main changes:

- Removes the `ProductTabs` component since it's not needed for ticket products.
- Shows the `TicketLayout` component on the right side of the product images.

### Retrieve Ticket Product with Product

Next, you'll update the server function that retrieves products to include ticket products. This allows you to access the ticket product details when fetching a product.

In `src/lib/data/products.ts`, find the `listProducts` function and update the `fields` parameter passed to the SDK call to include the `*ticket_product` relation:

```ts title="src/lib/data/products.ts" highlights={[["18"]]}
export const listProducts = async ({
  // ...
}: {
  // ...
}): Promise<{
  // ...
}> => {
  // ...
  return sdk.client
    .fetch<{
      // ...
    }>(
      `/store/products`,
      {
        // ...
        query: {
          // ...
          fields: "*variants.calculated_price,+variants.inventory_quantity,+metadata,+tags,*ticket_product",
        },
      }
    )
  // ...
}
```

This updates the `fields` parameter to include `*ticket_product`, which retrieves the ticket product relation when fetching products.

### Change Modal Styling

To ensure the height of the modal can hold the seat selection component, you'll need to adjust the modal styling.

In `src/modules/common/components/modal/index.tsx`, change the `classNames` of the `Dialog.Panel` to include `max-h-[90vh] overflow-y-auto`:

```tsx title="src/modules/common/components/modal/index.tsx" highlights={[["3"]]}
<Dialog.Panel
  className={clx(
    "flex flex-col justify-start w-full transform p-5 text-left align-middle transition-all max-h-[90vh] h-fit overflow-y-auto",
    {
      // ...
    }
  )}
  // ...
>
  {/* ... */}
</Dialog.Panel>
```

### Add Toaster to Layout

Since you used the `toast` function in your customization to show toast messages, you need to ensure the `Toaster` component is included in your layout.

In `src/app/layout.tsx`, add the following import at the top of the file:

```tsx title="src/app/layout.tsx"
import { Toaster } from "@medusajs/ui"
```

Then, add the `<Toaster />` component at the end of the body tag in the `return` statement:

```tsx title="src/app/layout.tsx" highlights={[["5"]]}
return (
  <html lang="en" data-mode="light">
    <body>
      <main className="relative">{props.children}</main>
      <Toaster />
    </body>
  </html>
)
```

### Test Product Page

To test the product page in the Next.js Starter Storefront, run the following command in the Medusa application's directory:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

And run the following command in the Next.js Starter Storefront application's directory:

```bash npm2yarn
npm run dev
```

Then:

1. Open the storefront at `http://localhost:8000`.
2. Click on Menu in the navbar, and choose Store.
3. In the store page, click on a ticket product you created.

You'll see on the product page the date selector component on the right side of the product images.

![Product page for a ticket product with date selector](https://res.cloudinary.com/dza7lstvk/image/upload/v1757498728/Medusa%20Resources/CleanShot_2025-09-10_at_13.05.15_2x_ktdgil.png)

Select a date and number of tickets, then click the "Pick Seats" button. A modal will open with the seat selection component. You can select seats and click the "Buy Tickets" button to add them to the cart and proceed to checkout.

![Seat selection modal on the product page](https://res.cloudinary.com/dza7lstvk/image/upload/v1757496623/Medusa%20Resources/CleanShot_2025-09-10_at_12.30.07_2x_ylhhk1.png)

This will open the checkout page with the selected tickets in the cart. You'll customize the checkout and cart components next.

***

## Step 4: Customize Cart and Checkout

In this section, you'll customize the cart and checkout components and functions to:

1. Display the selected seats for items, which appears in the cart and checkout pages.
2. Remove the Shipping Address and Shipping Method steps from the checkout process since ticket products don't require shipping. Instead, you'll only collect the billing address.
3. Update the function that retrieves the cart to retrieve its billing address.
4. Update the function that saves the address to only save the billing address.
5. Use the custom cart completion API route to place an order.

### Display Selected Seats in Cart and Checkout

First, you'll customize the cart item component to display the selected seats for ticket products.

In `src/modules/cart/components/item/index.tsx`, find the `LineItemOptions` component in the `return` statement of the `Item` component, and replace it with the following:

```tsx title="src/modules/cart/components/item/index.tsx"
{item.metadata?.seat_number !== undefined && (
  <Text className="txt-medium text-ui-fg-subtle">
    Seat {item.metadata?.row_number as string}{item.metadata?.seat_number as string}
  </Text>
)}
{item.metadata?.show_date !== undefined && (
  <Text className="txt-medium text-ui-fg-subtle">
    Show Date: {new Date(item.metadata?.show_date as string).toLocaleDateString()}
  </Text>
)}
```

This displays the selected seat number and show date for ticket products instead of the variant name.

You should also remove the quantity selector for ticket products since you can't purchase multiple tickets for the same seat. Remove the following highlighted lines from the `return` statement of the `Item` component:

```tsx title="src/modules/cart/components/item/index.tsx"
{type === "full" && (
  <Table.Cell>
    <div className="flex gap-2 items-center w-28">
      <DeleteButton id={item.id} data-testid="product-delete-button" />
      <CartItemSelect
        value={item.quantity}
        onChange={(value) => changeQuantity(parseInt(value.target.value))}
        className="w-14 h-10 p-4"
        data-testid="product-select-button"
      >
        {/* TODO: Update this with the v2 way of managing inventory */}
        {Array.from(
          {
            length: Math.min(maxQuantity, 10),
          },
          (_, i) => (
            <option value={i + 1} key={i}>
              {i + 1}
            </option>
          )
        )}

        <option value={1} key={1}>
          1
        </option>
      </CartItemSelect>
      {updating && <Spinner />}
    </div>
    <ErrorMessage error={error} data-testid="product-error-message" />
  </Table.Cell>
)}
```

This removes the quantity selector and error message for ticket products.

You can also remove the quantity indicator later in the component. Remove the following line from the `return` statement of the `Item` component:

```tsx title="src/modules/cart/components/item/index.tsx"
<Text className="text-ui-fg-muted">{item.quantity}x </Text>
```

This prevents displaying the quantity for ticket products, as it's redundant.

### Add Billing Address Step

Next, you'll add a component that shows a step to enter the billing address during checkout. You'll later use this component in the checkout page.

Create the file `src/modules/checkout/components/billing-address/index.tsx` with the following content:

```tsx title="src/modules/checkout/components/billing-address/index.tsx" collapsibleLines="1-8" expandButtonLabel="Show Imports"
import { HttpTypes } from "@medusajs/types"
import { Container } from "@medusajs/ui"
import Input from "@modules/common/components/input"
import { mapKeys } from "lodash"
import React, { useEffect, useMemo, useState } from "react"
import AddressSelect from "../address-select"
import CountrySelect from "../country-select"

const BillingAddress = ({
  customer,
  cart,
}: {
  customer: HttpTypes.StoreCustomer | null
  cart: HttpTypes.StoreCart | null
}) => {
  const [formData, setFormData] = useState<Record<string, any>>({
    "billing_address.first_name": cart?.billing_address?.first_name || "",
    "billing_address.last_name": cart?.billing_address?.last_name || "",
    "billing_address.address_1": cart?.billing_address?.address_1 || "",
    "billing_address.company": cart?.billing_address?.company || "",
    "billing_address.postal_code": cart?.billing_address?.postal_code || "",
    "billing_address.city": cart?.billing_address?.city || "",
    "billing_address.country_code": cart?.billing_address?.country_code || "",
    "billing_address.province": cart?.billing_address?.province || "",
    "billing_address.phone": cart?.billing_address?.phone || "",
    email: cart?.email || "",
  })

  const countriesInRegion = useMemo(
    () => cart?.region?.countries?.map((c) => c.iso_2),
    [cart?.region]
  )

  // check if customer has saved addresses that are in the current region
  const addressesInRegion = useMemo(
    () =>
      customer?.addresses.filter(
        (a) => a.country_code && countriesInRegion?.includes(a.country_code)
      ),
    [customer?.addresses, countriesInRegion]
  )

  const setFormAddress = (
    address?: HttpTypes.StoreCartAddress,
    email?: string
  ) => {
    address &&
      setFormData((prevState: Record<string, any>) => ({
        ...prevState,
        "billing_address.first_name": address?.first_name || "",
        "billing_address.last_name": address?.last_name || "",
        "billing_address.address_1": address?.address_1 || "",
        "billing_address.company": address?.company || "",
        "billing_address.postal_code": address?.postal_code || "",
        "billing_address.city": address?.city || "",
        "billing_address.country_code": address?.country_code || "",
        "billing_address.province": address?.province || "",
        "billing_address.phone": address?.phone || "",
      }))

    email &&
      setFormData((prevState: Record<string, any>) => ({
        ...prevState,
        email: email,
      }))
  }

  useEffect(() => {
    // Ensure cart is not null and has a billing_address before setting form data
    if (cart && cart.billing_address) {
      setFormAddress(cart?.billing_address, cart?.email)
    }

    if (cart && !cart.email && customer?.email) {
      setFormAddress(undefined, customer.email)
    }
  }, [cart]) // Add cart as a dependency

  const handleChange = (
    e: React.ChangeEvent<
      HTMLInputElement | HTMLInputElement | HTMLSelectElement
    >
  ) => {
    setFormData({
      ...formData,
      [e.target.name]: e.target.value,
    })
  }

  return (
    <>
      {customer && (addressesInRegion?.length || 0) > 0 && (
        <Container className="mb-6 flex flex-col gap-y-4 p-5">
          <p className="text-small-regular">
            {`Hi ${customer.first_name}, do you want to use one of your saved addresses?`}
          </p>
          <AddressSelect
            addresses={customer.addresses}
            addressInput={
              mapKeys(formData, (_, key) =>
                key.replace("billing_address.", "")
              ) as HttpTypes.StoreCartAddress
            }
            onSelect={setFormAddress}
          />
        </Container>
      )}
      <div className="grid grid-cols-2 gap-4">
        <Input
          label="First name"
          name="billing_address.first_name"
          autoComplete="given-name"
          value={formData["billing_address.first_name"]}
          onChange={handleChange}
          required
          data-testid="shipping-first-name-input"
        />
        <Input
          label="Last name"
          name="billing_address.last_name"
          autoComplete="family-name"
          value={formData["billing_address.last_name"]}
          onChange={handleChange}
          required
          data-testid="shipping-last-name-input"
        />
        <Input
          label="Address"
          name="billing_address.address_1"
          autoComplete="address-line1"
          value={formData["billing_address.address_1"]}
          onChange={handleChange}
          required
          data-testid="shipping-address-input"
        />
        <Input
          label="Company"
          name="billing_address.company"
          value={formData["billing_address.company"]}
          onChange={handleChange}
          autoComplete="organization"
          data-testid="shipping-company-input"
        />
        <Input
          label="Postal code"
          name="billing_address.postal_code"
          autoComplete="postal-code"
          value={formData["billing_address.postal_code"]}
          onChange={handleChange}
          required
          data-testid="shipping-postal-code-input"
        />
        <Input
          label="City"
          name="billing_address.city"
          autoComplete="address-level2"
          value={formData["billing_address.city"]}
          onChange={handleChange}
          required
          data-testid="shipping-city-input"
        />
        <CountrySelect
          name="billing_address.country_code"
          autoComplete="country"
          region={cart?.region}
          value={formData["billing_address.country_code"]}
          onChange={handleChange}
          required
          data-testid="shipping-country-select"
        />
        <Input
          label="State / Province"
          name="billing_address.province"
          autoComplete="address-level1"
          value={formData["billing_address.province"]}
          onChange={handleChange}
          data-testid="shipping-province-input"
        />
        <Input
          label="Email"
          name="email"
          type="email"
          title="Enter a valid email address."
          autoComplete="email"
          value={formData.email}
          onChange={handleChange}
          required
          data-testid="shipping-email-input"
        />
        <Input
          label="Phone"
          name="billing_address.phone"
          autoComplete="tel"
          value={formData["billing_address.phone"]}
          onChange={handleChange}
          data-testid="shipping-phone-input"
        />
      </div>
    </>
  )
}

export default BillingAddress
```

This displays the billing address form with the relevant fields. If the customer has saved addresses, it also displays an address selector at the top.

### Update Checkout Steps

Next, you'll update the checkout steps to remove the shipping address and shipping method steps, and add the billing address step.

In `src/modules/checkout/templates/checkout-form/index.tsx`, remove the `shippingMethods` variable in the `CheckoutForm` component and change the condition before the `return` statement:

```tsx title="src/modules/checkout/templates/checkout-form/index.tsx" highlights={[["10"], ["14"]]}
export default async function CheckoutForm({
  cart,
  customer,
}: {
  cart: HttpTypes.StoreCart | null
  customer: HttpTypes.StoreCustomer | null
}) {
  // ...
  // REMOVE THIS LINE
  // const shippingMethods = await listCartShippingMethods(cart.id)
  const paymentMethods = await listCartPaymentMethods(cart.region?.id ?? "")

  // Change this condition to only check for payment methods
  if (!paymentMethods) {
    return null
  }
  // ...
}
```

This removes the `shippingMethods` variable and changes the condition to only check for payment methods since shipping is not needed for ticket products.

Then, remove the `Shipping` component from the `return` statement. The `return` statement should look like this:

```tsx title="src/modules/checkout/templates/checkout-form/index.tsx"
return (
  <div className="w-full grid grid-cols-1 gap-y-8">
    <Addresses cart={cart} customer={customer} />

    <Payment cart={cart} availablePaymentMethods={paymentMethods} />

    <Review cart={cart} />
  </div>
)
```

This removes the `Shipping` component since shipping is not needed for ticket products.

Next, you'll update the `Addresses` component to use the `BillingAddress` component you created earlier.

In `src/modules/checkout/components/addresses/index.tsx`, add the following import at the top of the file:

```tsx title="src/modules/checkout/components/addresses/index.tsx"
import BillingAddress from "../billing-address"
```

Then, you'll change the `return` statement of the `Addresses` component to:

1. Use the `BillingAddress` component instead of the `ShippingAddress` component.
2. Update the header text to "Billing Address".
3. Replace all instances of `shipping_address` with `billing_address`.
4. Remove the same-as-billing checkbox and related logic.
5. Change the button that proceeds to the next step to say "Continue to payment".

The `return` statement should look like this:

```tsx title="src/modules/checkout/components/addresses/index.tsx"
return (
  <div className="bg-white">
    <div className="flex flex-row items-center justify-between mb-6">
      <Heading
        level="h2"
        className="flex flex-row text-3xl-regular gap-x-2 items-baseline"
      >
        Billing Address
        {!isOpen && <CheckCircleSolid />}
      </Heading>
      {!isOpen && cart?.billing_address && (
        <Text>
          <button
            onClick={handleEdit}
            className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover"
            data-testid="edit-address-button"
          >
            Edit
          </button>
        </Text>
      )}
    </div>
    {isOpen ? (
      <form action={formAction}>
        <div className="pb-8">
          <BillingAddress
            customer={customer}
            cart={cart}
          />
          <SubmitButton className="mt-6" data-testid="submit-address-button">
            Continue to payment
          </SubmitButton>
          <ErrorMessage error={message} data-testid="address-error-message" />
        </div>
      </form>
    ) : (
      <div>
        <div className="text-small-regular">
          {cart && cart.billing_address ? (
            <div className="flex items-start gap-x-8">
              <div className="flex items-start gap-x-1 w-full">
                <div
                  className="flex flex-col w-1/3"
                  data-testid="billing-address-summary"
                >
                  <Text className="txt-medium-plus text-ui-fg-base mb-1">
                    Billing Address
                  </Text>
                  <Text className="txt-medium text-ui-fg-subtle">
                    {cart.billing_address.first_name}{" "}
                    {cart.billing_address.last_name}
                  </Text>
                  <Text className="txt-medium text-ui-fg-subtle">
                    {cart.billing_address.address_1}{" "}
                    {cart.billing_address.address_2}
                  </Text>
                  <Text className="txt-medium text-ui-fg-subtle">
                    {cart.billing_address.postal_code},{" "}
                    {cart.billing_address.city}
                  </Text>
                  <Text className="txt-medium text-ui-fg-subtle">
                    {cart.billing_address.country_code?.toUpperCase()}
                  </Text>
                </div>

                <div
                  className="flex flex-col w-1/3 "
                  data-testid="billing-contact-summary"
                >
                  <Text className="txt-medium-plus text-ui-fg-base mb-1">
                    Contact
                  </Text>
                  <Text className="txt-medium text-ui-fg-subtle">
                    {cart.billing_address.phone}
                  </Text>
                  <Text className="txt-medium text-ui-fg-subtle">
                    {cart.email}
                  </Text>
                </div>

                <div
                  className="flex flex-col w-1/3"
                  data-testid="billing-address-summary"
                >
                  <Text className="txt-medium-plus text-ui-fg-base mb-1">
                    Billing Address
                  </Text>
                </div>
              </div>
            </div>
          ) : (
            <div>
              <Spinner />
            </div>
          )}
        </div>
      </div>
    )}
    <Divider className="mt-8" />
  </div>
)
```

Next, you'll update other steps to remove the checks for the shipping address and shipping method.

In `src/modules/checkout/components/payment/index.tsx`, find the `paymentReady` variable in the `Payment` component and change it to the following:

```tsx title="src/modules/checkout/components/payment/index.tsx"
const paymentReady =
  activeSession || paidByGiftcard
```

Similarly, in `src/modules/checkout/components/payment-button/index.tsx`, find the `notReady` variable in the `PaymentButton` component and change it to the following:

```tsx title="src/modules/checkout/components/payment-button/index.tsx"
const notReady =
  !cart ||
  !cart.billing_address ||
  !cart.email
```

Finally, in `src/modules/checkout/components/review/index.tsx`, find the `previousStepsCompleted` variable in the `Review` component and change it to the following:

```tsx title="src/modules/checkout/components/review/index.tsx"
const previousStepsCompleted =
  cart.billing_address &&
  (cart.payment_collection || paidByGiftcard)
```

### Update Cart Functions

Next, you'll update cart server functions to retrieve and save the billing address instead of the shipping address, and to use the custom cart completion API route.

In `src/lib/data/cart.ts`, find the `retrieveCart` function and update the `fields` variable to include `billing_address`:

```ts title="src/lib/data/cart.ts"
export async function retrieveCart(cartId?: string, fields?: string) {
  fields ??= "*items, *region, *items.product, *items.variant, *items.thumbnail, *items.metadata, +items.total, *promotions, +shipping_methods.name, *billing_address"
  // ...
}
```

Then, you need to update the `setAddresses` function in the same file to only save billing address information. Replace the entire function with the following:

```ts title="src/lib/data/cart.ts"
export async function setAddresses(currentState: unknown, formData: FormData) {
  try {
    if (!formData) {
      throw new Error("No form data found when setting addresses")
    }
    const cartId = getCartId()
    if (!cartId) {
      throw new Error("No existing cart found when setting addresses")
    }

    const data = {
      billing_address: {
        first_name: formData.get("billing_address.first_name"),
        last_name: formData.get("billing_address.last_name"),
        address_1: formData.get("billing_address.address_1"),
        address_2: "",
        company: formData.get("billing_address.company"),
        postal_code: formData.get("billing_address.postal_code"),
        city: formData.get("billing_address.city"),
        country_code: formData.get("billing_address.country_code"),
        province: formData.get("billing_address.province"),
        phone: formData.get("billing_address.phone"),
      },
      email: formData.get("email"),
    } as any
    await updateCart(data)
  } catch (e: any) {
    return e.message
  }

  redirect(
    `/${formData.get("billing_address.country_code")}/checkout?step=payment`
  )
}
```

This updates the function to only handle the billing address and email fields. It also redirects to the `payment` step of checkout instead of the `shipping` step.

Finally, you'll update the `placeOrder` function to use the custom cart completion API route you created in the [backend part of the tutorial](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/ticket-booking/example#custom-complete-cart-workflow/index.html.md). Replace the entire function with the following:

```ts title="src/lib/data/cart.ts"
export async function placeOrder(cartId?: string) {
  const id = cartId || (await getCartId())

  if (!id) {
    throw new Error("No existing cart found when placing an order")
  }

  const headers = {
    ...(await getAuthHeaders()),
  }

  const cartRes = await sdk.client.fetch<HttpTypes.StoreCompleteCartResponse>(`/store/carts/${id}/complete-tickets`, {
    method: "POST",
    headers,
  })
    .then(async (cartRes) => {
      const cartCacheTag = await getCacheTag("carts")
      revalidateTag(cartCacheTag)
      return cartRes
    })
    .catch(medusaError)

  if (cartRes?.type === "order") {
    const countryCode =
      cartRes.order.billing_address?.country_code?.toLowerCase()

    const orderCacheTag = await getCacheTag("orders")
    revalidateTag(orderCacheTag)

    removeCartId()
    redirect(`/${countryCode}/order/${cartRes?.order.id}/confirmed`)
  }

  return cartRes.cart
}
```

This updates the function to call the `/store/carts/{id}/complete-tickets` endpoint to complete the cart and place the order. It also retrieves the country code from the billing address instead of the shipping address to redirect to the order confirmation page.

### Test Checkout Page

To test the latest cart and checkout changes, start both the Medusa application and the Next.js Starter Storefront.

Then, add a ticket product to the cart by selecting a date, quantity, and seats on the product page. You'll be taken to the checkout page where you can see only three steps: Billing Address, Payment, and Review.

![Checkout page with three steps: Billing Address, Payment, and Review](https://res.cloudinary.com/dza7lstvk/image/upload/v1757500461/Medusa%20Resources/CleanShot_2025-09-10_at_13.34.07_2x_bkevfs.png)

Fill out the billing address form, choose a payment method, and click "Place Order" in the Review step. Once the order is placed, you'll be redirected to the order confirmation page. You may see an error related to shipping details missing, which you'll fix by customizing the order confirmation page next.

***

## Step 5: Customize Order Confirmation Page

The last storefront customization you'll make is to:

- Display billing details instead of shipping details on the order confirmation page.
- Display the selected seats and show date for each ticket item on the order confirmation page.

### Add Billing Details Component

First, you'll create a component that displays the billing details on the order confirmation page.

Create the file `src/modules/order/components/billing-details/index.tsx` with the following content:

```tsx title="src/modules/order/components/billing-details/index.tsx"
import { convertToLocale } from "@lib/util/money"
import { HttpTypes } from "@medusajs/types"
import { Heading, Text } from "@medusajs/ui"

import Divider from "@modules/common/components/divider"

type BillingDetailsProps = {
  order: HttpTypes.StoreOrder
}

const BillingDetails = ({ order }: BillingDetailsProps) => {
  return (
    <div>
      <Heading level="h2" className="flex flex-row text-3xl-regular my-6">
        Billing Address
      </Heading>
      <div className="flex items-start gap-x-8">
        <div
          className="flex flex-col w-1/3"
          data-testid="shipping-address-summary"
        >
          <Text className="txt-medium-plus text-ui-fg-base mb-1">
            Billing Address
          </Text>
          <Text className="txt-medium text-ui-fg-subtle">
            {order.billing_address?.first_name}{" "}
            {order.billing_address?.last_name}
          </Text>
          <Text className="txt-medium text-ui-fg-subtle">
            {order.billing_address?.address_1}{" "}
            {order.billing_address?.address_2}
          </Text>
          <Text className="txt-medium text-ui-fg-subtle">
            {order.billing_address?.postal_code},{" "}
            {order.billing_address?.city}
          </Text>
          <Text className="txt-medium text-ui-fg-subtle">
            {order.billing_address?.country_code?.toUpperCase()}
          </Text>
        </div>

        <div
          className="flex flex-col w-1/3 "
          data-testid="billing-contact-summary"
        >
          <Text className="txt-medium-plus text-ui-fg-base mb-1">Contact</Text>
          <Text className="txt-medium text-ui-fg-subtle">
            {order.billing_address?.phone}
          </Text>
          <Text className="txt-medium text-ui-fg-subtle">{order.email}</Text>
        </div>
      </div>
      <Divider className="mt-8" />
    </div>
  )
}

export default BillingDetails
```

This component displays the billing address and contact information from the order.

Then, you'll update the order confirmation and order details pages to use the `BillingDetails` component.

In `src/modules/order/templates/order-completed-template.tsx`, add the following import at the top of the file:

```tsx title="src/modules/order/templates/order-completed-template.tsx"
import BillingDetails from "@modules/order/components/billing-details"
```

And replace the `ShippingDetails` component in the `return` statement of the `OrderCompletedTemplate` component with the `BillingDetails` component:

```tsx title="src/modules/order/templates/order-completed-template.tsx"
<BillingDetails order={order} />
```

Similarly, in `src/modules/order/templates/order-details-template.tsx`, add the following import at the top of the file:

```tsx title="src/modules/order/templates/order-details-template.tsx"
import BillingDetails from "@modules/order/components/billing-details"
```

And replace the `ShippingDetails` component in the `return` statement of the `OrderDetailsTemplate` component with the `BillingDetails` component:

```tsx title="src/modules/order/templates/order-details-template.tsx"
<BillingDetails order={order} />
```

The order confirmation page now should show the billing details instead of shipping details.

![Order confirmation page showing billing details](https://res.cloudinary.com/dza7lstvk/image/upload/v1757504448/Medusa%20Resources/CleanShot_2025-09-10_at_14.40.34_2x_pwfyyf.png)

### Show Selected Seats in Order Items

Next, you'll update the order item component to display the selected seats and show date for ticket products.

In `src/modules/order/components/item/index.tsx`, find the `LineItemOptions` component in the `return` statement of the `Item` component, and replace it with the following:

```tsx title="src/modules/order/components/item/index.tsx"
{item.metadata?.seat_number !== undefined && (
  <Text className="txt-medium text-ui-fg-subtle">
    Seat {item.metadata?.row_number as string}{item.metadata?.seat_number as string}
  </Text>
)}
{item.metadata?.show_date !== undefined && (
  <Text className="txt-medium text-ui-fg-subtle">
    Show Date: {new Date(item.metadata?.show_date as string).toLocaleDateString()}
  </Text>
)}
```

You can also remove the quantity by removing the following line from the `return` statement of the `Item` component:

```tsx title="src/modules/order/components/item/index.tsx"
<Text className="text-ui-fg-muted">
  <span data-testid="product-quantity">{item.quantity}</span>x{" "}
</Text>
```

This prevents displaying the quantity for ticket products, as it's redundant.

If you check the order confirmation page now, you'll see the selected seats and show dates for each ticket item.

![Order confirmation page showing selected seats and show date for ticket items](https://res.cloudinary.com/dza7lstvk/image/upload/v1757504846/Medusa%20Resources/CleanShot_2025-09-10_at_14.47.16_2x_mfnh3f.png)

***

## Next Steps

You've now customized the Next.js Starter Storefront to support ticket products. You can further customize the storefront to change the styling and user experience to fit your brand.

### Learn More About Medusa

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.


# Ticket Booking System Recipe

This recipe provides the general steps to implement a ticket booking system in your Medusa application.

Follow the step-by-step [Ticket Booking System Example](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/ticket-booking/example/index.html.md) to learn how to implement a ticket booking system in your Medusa application.

## Overview

A ticket booking system allows customers to book tickets for events, such as shows or sports games. By using a ticket booking system, you can manage shows, venues, and seat availability, among other features.

Medusa's [Framework](https://docs.medusajs.com/docs/learn/fundamentals/framework/index.html.md) facilitates building a ticket booking system on top of the existing commerce functionalities, such as products, pricing, inventory, carts, and orders. All of these are provided by Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

To support ticket-booking features, you can customize the Medusa application by creating a Ticket Booking Module, linking its data models to Medusa's existing models, and building flows around the module.

***

## Create Ticket Booking Module

Your custom features and functionalities are implemented inside modules. The module is integrated into the Medusa application without any implications on existing functionalities.

The module will hold your custom data models and the service implementing ticket-booking-related features.

[How to Create a Module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md): Learn how to create a module.

### Create Custom Data Models

A data model represents a table in the database. You can define in your module data models to store data related to your custom features, such as shows, venues, and seats.

For example, you can define:

- A `Venue` model for the venue where the event takes place.
- A `TicketProduct` model for a show, and a `TicketProductVariant` model for the ticket variants (for example, different seating sections).
- A `TicketPurchase` model for a ticket purchase.

Then, you can link your custom data model to data models from other modules. For example, you can link:

- The `TicketProduct` model to the Product Module's `Product` data model, benefiting from existing features related to products and collections.
- The `TicketProductVariant` model to the Product Module's `ProductVariant` data model, benefiting from existing features related to inventory and pricing.
- The `TicketPurchase` model to the Order Module's `Order` data model, benefiting from existing features related to orders and payments.

- [How to Create a Data Model](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md): Learn how to create a data model.
- [Define Module Links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md): Define links between data models.

### Implement Data Management Features

Your module’s main service holds data-management and other related features. Then, in other resources, such as an API route, you can resolve the service from the Medusa container and use its functionalities.

Medusa facilitates implementing data-management features using the service factory. Your module's main service can extend this service factory, and it generates data-management methods for your data models.

[Service Factory](https://docs.medusajs.com/docs/learn/fundamentals/modules/service-factory/index.html.md): Learn about the service factory and how to use it.

***

## Implement Ticket Booking Business Flows

You can implement ticket-booking-related business logic in workflows. A workflow is a series of queries and actions, called steps, that complete a task.

By using workflows, you benefit from features like rollback mechanisms, error handling, and retrying failed steps. You can then execute workflows from other resources, such as an API route.

You can implement workflows that create venues and ticket products, complete carts with ticket products, and more. In the workflow's steps, you can resolve the Ticket Booking Module's service and use its data-management methods to manage ticket-booking-related data.

You can then utilize the API routes that execute these workflows in your client applications, such as your Medusa Admin customizations or your storefront.

- [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md): Learn how to create a workflow.
- [API Routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md): Learn how to create API routes.

***

## Handle Ticket Products Inventory

By linking the `TicketProductVariant` model to the Product Module's `ProductVariant` model, you can leverage Medusa's inventory management features, implemented in the Inventory Module, to manage ticket product variants' inventory.

You can set up inventory items for ticket product variants based on show dates and seating sections. You can set the available quantity for each inventory item to reflect the number of seats available in that section for that show date.

This setup ensures that customers can only book tickets for available seats, preventing overbooking.

- [Inventory Module](https://docs.medusajs.com/commerce-modules/inventory/index.html.md): Learn about the Inventory Module and its features.
- [Product Variant Inventory](https://docs.medusajs.com/commerce-modules/product/variant-inventory/index.html.md): Learn how to manage product variant inventory.

***

## Disable Shipping on Ticket Product Variants

By default, Medusa product variants require shipping, which prompts the customer to provide a shipping address and choose a shipping method during checkout.

You can disable shipping on ticket product variants by setting the `requires_shipping` property of the product variant's inventory item to `false`.

Then, you can remove shipping-related steps from the checkout flow in the storefront.

[Configure Shipping Requirements](https://docs.medusajs.com/commerce-modules/product/selling-products#configure-shipping-requirements/index.html.md): Learn how to configure shipping requirements for product variants.

***

## Customize Admin Dashboard

You can extend the Medusa Admin to provide merchants with an interface to manage ticket-booking-related data, such as shows and venues. You can inject widgets into existing pages or create new pages.

In your customizations, you send requests to the API routes you created that execute the workflows implementing ticket-booking-related features.

- [Create a Widget](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md): Learn how to create a widget in the Medusa Admin.
- [Create UI Route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md): Learn how to create a UI route in the Medusa Admin.

***

## Add Custom Validation on Cart Operations

Medusa's workflows provide hooks that allow you to inject custom logic at specific points in the workflow execution.

Specifically, workflows like `addToCartWorkflow` and `completeCartWorkflow` provide a `validate` hook that you can consume to add custom validation logic.

For example, you can consume the `validate` hook in the `addToCartWorkflow` to check if the selected seat is available before adding a ticket product variant to the cart.

[Workflow Hooks](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md): Learn how to use workflow hooks.

***

## Send Order Confirmation Email with QR Code Ticket

When an order is placed, Medusa emits an `order.placed` event. You can listen to this event in a subscriber, which is an asynchronous function executed in the background when its associated event is emitted.

In the subscriber, you can send an email using the Notification Module. You can register a Notification Module Provider, such as [SendGrid](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md) or [Resend](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/integrations/guides/resend/index.html.md), that sends the email.

You can include a QR code representing the ticket in the email, which you can generate using a library like `qrcode`.

You can also implement an API route that verifies the QR code at the event entrance.

- [Events and Subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md): Learn how to create a subscriber.
- [Notification Module](https://docs.medusajs.com/infrastructure-modules/notification/index.html.md): Learn about available Notification Module Providers.

***

## Customize or Build Storefront

Customers can book tickets through your storefront. They need features like listing shows, selecting show dates and seats, and placing orders through a checkout flow that supports ticket products.

Medusa provides a Next.js Starter Storefront with standard commerce features including listing products, placing orders, and managing accounts. You can customize the storefront and tailor its functionalities to support ticket booking features.

Alternatively, you can build the storefront with your preferred tech stack.

- [Next.js Starter Storefront](https://docs.medusajs.com/nextjs-starter/index.html.md): Learn how to install and use the Next.js Starter Storefront.
- [Storefront Guides](https://docs.medusajs.com/storefront-development/index.html.md): Learn how to build a storefront for your Medusa application.
