---
sidebar_label: "Implement Quote Management"
tags:
    - cart
    - order
    - server
    - tutorial
products:
  - cart
  - order
  - customer
---

import { Github, PlaySolid } from "@medusajs/icons"
import { Prerequisites, WorkflowDiagram } from "docs-ui"

export const ogImage = "https://res.cloudinary.com/dza7lstvk/image/upload/v1741682814/Medusa%20Resources/quote_ldmqaq.jpg"

export const metadata = {
  title: `Implement Quote Management in Medusa`,
  openGraph: {
    images: [
      {
        url: ogImage,
        width: 1600,
        height: 836,
        type: "image/jpeg"
      }
    ],
  },
  twitter: {
    images: [
      {
        url: ogImage,
        width: 1600,
        height: 836,
        type: "image/jpeg"
      }
    ]
  }
}

# {metadata.title}

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](../../../commerce-modules/page.mdx) 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.

<Note title="Tip">

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.

</Note>

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

<CardList items={[
  {
    href: "https://github.com/medusajs/examples/tree/main/quote-management",
    title: "Quote Management Repository",
    text: "Find the full code for this guide in this repository.",
    icon: Github,
  },
  {
    href: "https://res.cloudinary.com/dza7lstvk/raw/upload/v1741171875/OpenApi/quote-management_tbk552.yml",
    title: "OpenApi Specs for Postman",
    text: "Import this OpenApi Specs file into tools like Postman.",
    icon: PlaySolid,
  },
]} />

---

## Step 1: Install a Medusa Application

<Prerequisites items={[
  {
    text: "Node.js v20+",
    link: "https://nodejs.org/en/download"
  },
  {
    text: "Git CLI tool",
    link: "https://git-scm.com/downloads"
  },
  {
    text: "PostgreSQL",
    link: "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](../../../nextjs-starter/page.mdx).

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.

<Note title="Why is the storefront installed separately">

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](!docs!/learn/fundamentals/api-routes). Learn more about Medusa's architecture in [this documentation](!docs!/learn/introduction/architecture).

</Note>

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.

<Note title="Ran into Errors">

Check out the [troubleshooting guides](../../../troubleshooting/create-medusa-app-errors/page.mdx) for help.

</Note>

---

## Step 2: Add Quote Module

In Medusa, you can build custom features in a [module](!docs!/learn/fundamentals/modules). 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.

<Note>

Learn more about modules in [this documentation](!docs!/learn/fundamentals/modules).

</Note>

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

<Note>

Learn more about data models in [this documentation](!docs!/learn/fundamentals/modules#1-create-data-model).

</Note>

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)

export const quoteModelHighlights = [
  ["12", "id", "A unique identifier for the quote."],
  ["13", "status", "The status of the quote."],
  ["16", "customer_id", "The ID of the customer who requested the quote."],
  ["17", "draft_order_id", "The ID of the draft order created for the quote."],
  ["18", "order_change_id", "The ID of the order change created for the quote."],
  ["19", "cart_id", "The ID of the cart that the quote was created from."],
]

```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](../../../commerce-modules/order/order-change/page.mdx) 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.

<Note>

Learn more about defining data model properties in the [Property Types documentation](!docs!/learn/fundamentals/data-models/properties).

</Note>

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

<Note>

Learn more about services in [this documentation](!docs!/learn/fundamentals/modules#2-create-service).

</Note>

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

<Note>

Find all methods generated by the `MedusaService` in [this reference](../../../service-factory-reference/page.mdx).

</Note>

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.

<Note>

Learn more about migrations in [this documentation](!docs!/learn/fundamentals/modules#5-generate-migrations).

</Note>

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](../../../commerce-modules/customer/page.mdx).

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.

<Note>

To learn more about module isolation, refer to the [Module Isolation documentation](!docs!/learn/fundamentals/modules/isolation).

</Note>

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](../../../commerce-modules/cart/page.mdx): link quotes to the carts they were created from.
2. `Quote` \<\> `Customer` data model of the [Customer Module](../../../commerce-modules/customer/page.mdx): link quotes to the customers who requested them.
3. `Quote` \<\> `OrderChange` data model of the [Order Module](../../../commerce-modules/order/page.mdx): 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](../../../commerce-modules/order/page.mdx): 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)

export const quoteCartHighlights = [
  ["7", "linkable", "Link configurations of the Quote Module's data models."],
  ["8", "field", "Specify the property that holds the ID of the linked record."],
  ["12", "readOnly", "Indicate that the link can only be used to retrieve linked records."],
]

```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](!docs!/learn/fundamentals/workflows). 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.

<Note>

Learn more about workflows in the [Workflows documentation](!docs!/learn/fundamentals/workflows).

</Note>

The workflow will have the following steps:

<WorkflowDiagram
  workflow={{
    name: "createRequestForQuoteWorkflow",
    steps: [
      {
        type: "step",
        name: "useQueryGraphStep",
        description: "Retrieve the cart that the customer wants a quote for.",
        link: "/references/helper-steps/useQueryGraphStep",
        depth: 1
      },
      {
        type: "step",
        name: "useQueryGraphStep",
        description: "Retrieve the customer requesting the quote.",
        link: "/references/helper-steps/useQueryGraphStep",
        depth: 1
      },
      {
        type: "workflow",
        name: "createOrderWorkflow",
        description: "Create the draft order for the quote.",
        link: "/references/medusa-workflows/createOrderWorkflow",
        depth: 1,
      },
      {
        type: "workflow",
        name: "beginOrderEditOrderWorkflow",
        description: "Create the order change for the draft order.",
        link: "/references/medusa-workflows/beginOrderEditOrderWorkflow",
        depth: 1
      },
      {
        type: "step",
        name: "createQuotesStep",
        description: "Create the quote for the customer.",
        depth: 1,
      }
    ]
  }}
  hideLegend
/>

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)

export const createQuotesStepHighlights = [
  ["15", "container", "Resolve the Quote Module's service from the Medusa container."],
  ["19", "createQuotes", "Create the quotes using the generated method."],
  ["22", "quotes", "Return the quotes as the step's output."],
  ["23", "map", "Pass the quote IDs to the compensation function."]
]

```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](!docs!/learn/fundamentals/medusa-container), 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](!docs!/learn/fundamentals/medusa-container).

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:

export const createRequestForQuoteHighlights = [
  ["24", "useQueryGraphStep", "Retrieve the cart's details."],
  ["45", "useQueryGraphStep", "Retrieve the customer's details."]
]

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

<Note title="Tip">

`useQueryGraphStep` uses [Query](!docs!/learn/fundamentals/module-links/query), 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](../../../commerce-modules/promotion/page.mdx), by passing `promotions.code` to the `fields` array.

</Note>

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](!docs!/learn/fundamentals/workflows/variable-manipulation) 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](!docs!/learn/fundamentals/api-routes).

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.

<Note>

Learn more about API routes in [this documentation](!docs!/learn/fundamentals/api-routes).

</Note>

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

export const createQuoteApiHighlights = [
  ["20", "createRequestForQuoteWorkflow", "Execute the workflow to create a quote."],
  ["33", "query", "Use Query to retrieve the quote."]
]

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

<Note title="Tip">

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

</Note>

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.

<Note>

Learn more about middleware in the [Middlewares documentation](!docs!/learn/fundamentals/api-routes/middlewares).

</Note>

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.

<Note>

Learn more about configuring Query for requests in the [Request Query Configurations documentation](!docs!/learn/fundamentals/module-links/query#request-query-configurations).

</Note>

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.

<Note>

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.

</Note>

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.

<Note>

To learn more about publishable API keys, refer to the [Publishable API Key documentation](../../../commerce-modules/sales-channel/publishable-api-keys/page.mdx).

</Note>

To retrieve the publishable API key from the Medusa Admin, refer to [this user guide](!user-guide!/settings/developer/publishable-api-keys).

#### 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](!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](!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](!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](!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](!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](!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](!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.

<Note>

For more accurate totals and processing of the quote's draft order, you should:

- Add shipping and billing addresses by [updating the cart](!api!/store#carts_postcartsid).
- [Choose a shipping method](!api!/store#carts_postcartsidshippingmethods) for the cart.
- [Create a payment collection](!api!/store#payment-collections_postpaymentcollections) for the cart.
- [Initialize payment session](!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](../../../storefront-development/checkout/page.mdx). It's not specific to quote management, so you'll need to change the last step to create a quote instead of an order.

</Note>

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

export const listQuotesHighlights = [
  ["10", "metadata", "Holds pagination parameters of the Query result."],
  ["10", "query", "Retrieve the list of quotes."],
  ["12", "req.queryConfig", "Pass the configured Query configurations for selected fields and pagination."]
]

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

<Note>

Learn more about paginating Query results in the [Query documentation](!docs!/learn/fundamentals/module-links/query#apply-pagination).

</Note>

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

<Note>

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.

</Note>

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.

<Note>

Learn more about UI routes in the [UI Routes documentation](!docs!/learn/fundamentals/admin/ui-routes).

</Note>

### Configure JS SDK

Medusa provides a [JS SDK](../../../js-sdk/page.mdx) 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](!ui!/components/data-table) 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.

<Note title="Tip">

Since you applied the middleware earlier to the `/admin/quotes*` route pattern, it will automatically apply to this route as well.

</Note>

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](!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)

export const quotesDetailsHighlights = [
  ["21", "useQuote", "Retrieve the quote's details from the Medusa server."],
  ["26", "useOrderPreview", "Retrieve the preview of the quote's draft order."]
]

```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:

<WorkflowDiagram
  workflow={{
    name: "merchantRejectQuoteWorkflow",
    steps: [
      {
        type: "step",
        name: "useQueryGraphStep",
        description: "Retrieve the quote's details.",
        link: "/references/helper-steps/useQueryGraphStep",
        depth: 1
      },
      {
        type: "step",
        name: "validateQuoteNotAccepted",
        description: "Validate that the quote isn't already accepted by the customer.",
        depth: 1
      },
      {
        type: "step",
        name: "updateQuoteStatusStep",
        description: "Update the quote's status to `merchant_rejected`.",
        depth: 1
      }
    ]
  }}
  hideLegend
/>

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)

export const merchantRejectionWorkflowHighlights = [
  ["14", "useQueryGraphStep", "Retrieve the quote's details."],
  ["23", "validateQuoteNotAccepted", "Validate that the quote isn't already accepted by the customer."],
  ["28", "updateQuotesStep", "Update the quote's status to `merchant_rejected`."]
]

```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:

<WorkflowDiagram
  workflow={{
    name: "merchantSendQuoteWorkflow",
    steps: [
      {
        type: "step",
        name: "useQueryGraphStep",
        description: "Retrieve the quote's details.",
        link: "/references/helper-steps/useQueryGraphStep",
        depth: 1
      },
      {
        type: "step",
        name: "validateQuoteNotAccepted",
        description: "Validate that the quote isn't already accepted by the customer.",
        depth: 1
      },
      {
        type: "step",
        name: "updateQuoteStatusStep",
        description: "Update the quote's status to `pending_customer`.",
        depth: 1
      }
    ]
  }}
  hideLegend
/>

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)

export const sendQuoteHighlights = [
  ["14", "useQueryGraphStep", "Retrieve the quote's details."],
  ["23", "validateQuoteNotAccepted", "Validate that the quote isn't already accepted by the customer."],
  ["28", "updateQuotesStep", "Update the quote's status to `pending_customer`."]
]

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

<Note>

You'll later add the feature to update the quote item's details before sending the quote back to the customer.

</Note>

![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:

<WorkflowDiagram
  workflow={{
    name: "customerRejectQuoteWorkflow",
    steps: [
      {
        type: "step",
        name: "useQueryGraphStep",
        description: "Retrieve the quote's details.",
        link: "/references/helper-steps/useQueryGraphStep",
        depth: 1
      },
      {
        type: "step",
        name: "validateQuoteNotAccepted",
        description: "Validate that the quote isn't already accepted by the customer.",
        depth: 1
      },
      {
        type: "step",
        name: "updateQuoteStatusStep",
        description: "Update the quote's status to `customer_rejected`.",
        depth: 1
      }
    ]
  }}
  hideLegend
/>

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)

export const customerRejectQuoteHighlights = [
  ["15", "useQueryGraphStep", "Retrieve the quote's details."],
  ["24", "validateQuoteNotAccepted", "Validate that the quote isn't already accepted by the customer."],
  ["29", "updateQuotesStep", "Update the quote's status to `customer_rejected`."]
]

```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:

<WorkflowDiagram
  workflow={{
    name: "customerAcceptQuoteWorkflow",
    steps: [
      {
        type: "step",
        name: "useQueryGraphStep",
        description: "Retrieve the quote's details.",
        link: "/references/helper-steps/useQueryGraphStep",
        depth: 1
      },
      {
        type: "step",
        name: "validateQuoteCanAcceptStep",
        description: "Validate that the quote can be accepted.",
        depth: 1
      },
      {
        type: "step",
        name: "updateQuotesStep",
        description: "Update the quote's status to `accepted`.",
        depth: 1
      },
      {
        type: "workflow",
        name: "confirmOrderEditRequestWorkflow",
        description: "Confirm the changes made on the draft order, such as changes to item quantities and prices.",
        link: "/references/medusa-workflows/confirmOrderEditRequestWorkflow",
        depth: 1
      },
      {
        type: "workflow",
        name: "updateOrderWorkflow",
        description: "Update the draft order to change its status and convert it into an order.",
        link: "/references/medusa-workflows/updateOrderWorkflow",
        depth: 1
      }
    ]
  }}
  hideLegend
/>

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)

export const customerAcceptQuoteHighlights = [
  ["20", "useQueryGraphStep", "Retrieve the quote's details."],
  ["29", "validateQuoteCanAcceptStep", "Validate that the quote can be accepted."],
  ["34", "updateQuotesStep", "Update the quote's status to `accepted`."],
  ["39", "confirmOrderEditRequestWorkflow", "Confirm the changes made on the draft order, such as changes to item quantities and prices."],
  ["46", "updateOrderWorkflow", "Update the draft order to change its status and convert it into an order."]
]

```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](../../../commerce-modules/order/order-change/page.mdx) 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](!api!/admin#order-edits) to provide the functionality to edit the quote's items.

<Note>

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](../../../commerce-modules/order/order-change/page.mdx) documentation to learn more.

</Note>

![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](!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](../../../storefront-development/page.mdx). 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](!docs!/learn), 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](../../../commerce-modules/page.mdx).
