From a4fd39e371d62e216cf718da71eceed529cccc1f Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Tue, 4 Mar 2025 17:33:12 +0200 Subject: [PATCH] initial --- .../examples/guides/quote-management/page.mdx | 2032 +++++++++++++++++ 1 file changed, 2032 insertions(+) create mode 100644 www/apps/resources/app/examples/guides/quote-management/page.mdx diff --git a/www/apps/resources/app/examples/guides/quote-management/page.mdx b/www/apps/resources/app/examples/guides/quote-management/page.mdx new file mode 100644 index 0000000000..37380ef48d --- /dev/null +++ b/www/apps/resources/app/examples/guides/quote-management/page.mdx @@ -0,0 +1,2032 @@ +--- +sidebar_title: "Quote Management" +tags: + - cart + - order + - server +--- + +import { Github, PlaySolid } from "@medusajs/icons" +import { Prerequisites, WorkflowDiagram } from "docs-ui" + +export const ogImage = "https://res.cloudinary.com/dza7lstvk/image/upload/v1738682676/Medusa%20Resources/guide-custom-item-pricing_lrilmb.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 supports a cart and order system. However, you can extend the application to support quote management. Quote management allows customers to request a quote for a set of products, and facilitates the negotiation process between the customer and the merchant. Quote management is useful in my use cases, including B2B stores. + + + +This guide is based on the [B2B starter](https://github.com/medusajs/b2b-starter-medusa) explaining how to implement some of its quote management features. You can refer to the B2B starter for more details on other features. + + + +## 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 product 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. + +To implement these features, you'll be customizing the Medusa backend and the Medusa Admin dashboard. + +You can follow this guide whether you're new to Medusa or an advanced Medusa developer. + +// TODO add image + + + +--- + +## Step 1: Install a Medusa Application + + + +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. + + + +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). + + + +Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard. + + + +Check out the [troubleshooting guides](../../../troubleshooting/create-medusa-app-errors/page.mdx) for help. + + + +--- + +## Step 2: Add Quote Module + +In Medusa, you 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. + + + +Learn more about modules in [this documentation](!docs!/learn/fundamentals/modules). + + + +### 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](https://res.cloudinary.com/dza7lstvk/image/upload/v1741074268/Medusa%20Resources/quote-1_lxgyyg.jpg) + +### Create Data Models + +A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations. + + + +Learn more about data models in [this documentation](!docs!/learn/fundamentals/modules#1-create-data-model). + + + +For the Quote Module, you need to define a `Quote` data model that represents a quote requested by a customer. + +So, start by creating the `Quote` data model. Create the file `src/modules/quote/models/quote.ts` with the following content: + +![Diagram showcasing the directory structure after adding the quote model](https://res.cloudinary.com/dza7lstvk/image/upload/v1741074453/Medusa%20Resources/quote-2_lh012l.jpg) + +```ts title="src/modules/quote/models/quote.ts" +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 with 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. 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. + +### Create Module's Service + +You now have the necessary data models in the Quote Module, but you need to define the logic to manage these data models. You do this by creating a service in the module. + +A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, allowing you to manage your data models, or connect to a third-party service, which is useful if you're integrating with external services. + + + +Learn more about data models in [this documentation](!docs!/learn/fundamentals/modules#2-create-service). + + + +To create the Quote Module's service, create the file `src/modules/quote/service.ts` with the following content: + +![Directory structure after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1741075946/Medusa%20Resources/quote-4_hg4bnr.jpg) + +```ts title="src/modules/quote/service.ts" +import { MedusaService } from "@medusajs/framework/utils"; +import { Quote } from "./models/quote"; + +class QuoteModuleService extends MedusaService({ + Quote, +}) {} + +export default QuoteModuleService; +``` + +The `QuoteModuleService` extends `MedusaService` from the Modules SDK which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods. + +So, the `QuoteModuleService` class now has methods like `createQuotes` and `retrieveQuote`. + + + +Find all methods generated by the `MedusaService` in [this reference](../../../service-factory-reference/page.mdx). + + + +You'll use this service later when you implement custom flows for quote management. + +### Export Module Definition + +The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service. + +So, create the file `src/modules/quote/index.ts` with the following content: + +![Directory structure after adding the module definition](https://res.cloudinary.com/dza7lstvk/image/upload/v1741076106/Medusa%20Resources/quote-5_ngitn1.jpg) + +```ts title="src/modules/quote/index.ts" +import { Module } from "@medusajs/framework/utils"; +import QuoteModuleService from "./service"; + +export const QUOTE_MODULE = "quote"; + +export default Module(QUOTE_MODULE, { + service: QuoteModuleService +}); +``` + +You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters: + +1. The module's name, which is `quote`. +2. An object with a required property `service` indicating the module's service. + +You also export the module's name as `QUOTE_MODULE` so you can reference it later. + +### Add Module to Medusa's Configurations + +Once you finish building the module, add it to Medusa's configurations to start using it. + +In `medusa-config.ts`, add a `modules` property and pass an array with your custom module: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/quote", + }, + ], +}) +``` + +Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name. + +### Generate Migrations + +Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module. + + + +Learn more about migrations in [this documentation](!docs!/learn/fundamentals/modules#5-generate-migrations). + + + +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 of the Quote Module's data model are now created in the database. + +--- + +## Step 3: Define Links to Other Modules + +When you defined the Quote Module's data models, you added properties that store the ID of records managed by other modules. For example, the `customer_id` property in the `Quote` data model stores the ID of a customer, who is managed by the [Customer Module](../../../commerce-modules/customer/page.mdx). + +As mentioned before, Medusa integrates modules into your application without implications or side effects. Medusa maintains this by isolating modules from one another. This means you can't directly create relationships between data models in your module and data models in other modules. + +Instead, Medusa provides the mechanism to define links between data models, and retrieve and manage linked records while maintaining module isolation. Links are useful to define associations between data models in different modules, or extend a model in another module to associate custom properties with it. + + + +To learn more about module isolation, refer to the [Module Isolation documentation](!docs!/learn/fundamentals/modules/isolation). + + + +In this step, you'll define the following links between the Quote Module's data models 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 define links between data models in a TypeScript or JavaScript file under the `src/links` directory. So, to define the link between the `Quote` and `Cart` data models, create the file `src/links/quote-cart.ts` with the following content: + +![Directory structure after adding the quote-cart link](https://res.cloudinary.com/dza7lstvk/image/upload/v1741077395/Medusa%20Resources/quote-7_xrvodi.jpg) + +```ts title="src/links/quote-cart.ts" +import { defineLink } from "@medusajs/framework/utils" +import QuoteModule from "../modules/quote" +import CartModule from "@medusajs/medusa/cart" + +export default defineLink( + { + ...QuoteModule.linkable.quote, + 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 the `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( + { + ...QuoteModule.linkable.quote, + 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` \<\> `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( + { + ...QuoteModule.linkable.quote, + 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( + { + ...QuoteModule.linkable.quote, + field: "draft_order_id" + }, + { + ...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 Flow + +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 products. + +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. + + + +Learn more about workflows in the [Workflows documentation](!docs!/learn/fundamentals/workflows). + + + +The workflow will have the following steps: + + + +The first four steps are provided by Medusa in its `@medusajs/medusa/core-flows` package. So, you only need to implement the `createQuotesStep` step. + +### createQuotesStep + +In the last step of the workflow, you'll create a quote for the customer using the Quote Module's service. + +To create a step, create the file `src/workflows/steps/create-quotes.ts` with the following content: + +![Directory structure after adding the create-quotes step](https://res.cloudinary.com/dza7lstvk/image/upload/v1741085446/Medusa%20Resources/quote-13_tv9i23.jpg) + +```ts title="src/workflows/steps/create-quotes.ts" +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 function of each step that ran before the error is 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: + +```ts title="src/workflows/create-request-for-quote.ts" +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) => { + // @ts-ignore + 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 the `useQueryGraphStep` helper to retrieve the cart and customer records using the IDs passed as an input. The `useQueryGraphStep` helper uses [Query](!docs!/learn/fundamentals/module-links/query) that allows you to retrieve data across modules. + +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. In the workflow constructor, you can't manipulate data directly as Medusa creates an internal representation of the workflow's constructor before these data actually have a value. 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 an 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. + +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-route). + +An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts. You'll create an API route at the path `/store/customers/me/quotes` that executes the workflow from the previous step. + + + +Learn more about API routes in [this documentation](!docs!/learn/fundamentals/api-routes). + + + +### Implement API Route + +An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`. + +By default, all routes starting with `/store/customers/me` require the customer to be authenticated. So, you'll be creating the API route at `/store/customers/me/quotes`. + +To create the API route, create the file `src/api/store/customers/me/quotes/route.ts` with the following content: + +![Directory structure after adding the store/quotes route](https://res.cloudinary.com/dza7lstvk/image/upload/v1741086995/Medusa%20Resources/quote-14_meo0yo.jpg) + +```ts title="src/api/store/customers/me/quotes/route.ts" +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, + res: MedusaResponse +) => { + const { + result: { quote: createdQuote }, + } = await createRequestForQuoteWorkflow(req.scope).run({ + input: { + ...req.validatedBody, + customer_id: req.auth_context.actor_id, + }, + }); + + const query = req.scope.resolve( + ContainerRegistrationKeys.QUERY + ); + + const { + data: [quote], + } = await query.graph( + { + entity: "quote", + fields: req.queryConfig.fields, + filters: { id: createdQuote.id }, + }, + { throwIfKeyNotFound: true } + ); + + return res.json({ quote }); +}; +``` + +Since you export a `POST` function in this file, you're exposing a `POST` API route at `/store/customers/me/quotes`. The route handler function accepts two parameters: + +1. A request object with details and context on the request, such as body parameters or authenticated customer details. +2. A response object to manipulate and send the response. + + + +`AuthenticatedMedusaRequest` accepts the request body's type as a type argument. + + + +In the route handler function, you create the quote using the [createRequestForQuoteWorkflow](#createrequestforquoteworkflow) from the previous step. Then, you resolve Query from the Medusa container, which is available in the request object's `req.scope` property. + +You use Query to retrieve the Quote with its fields and linked records, which you'll learn how to specify soon. Finally, you send the quote as a response. + +### Add Validation Schema + +The API route accepts the cart ID as a parameter. So, it's important to validate that it's actually passed in the request body of a request before executing its route 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; +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 + +// keep type argument the same +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + // ... +} +``` + +### Apply Validation Schema Middleware + +Now that you have the validation schema, you need to add the middleware that ensures the request body is validated before the route handler is executed. A middleware is a function executed when a request is sent to an API Route. It's executed before the route handler. + + + +Learn more about middleware in the [Middlewares documentation](!docs!/learn/fundamentals/api-routes/middlewares). + + + +Middlewares are created in the `src/api/middlewares.ts` file. However, as you'll add multiple middlewares for store and admin routes, you'll create the store middlewares in the `src/api/store/middlewares.ts` file then import them in `src/api/middlewares.ts`. + +So, create the file `src/api/store/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/store/middlewares.ts" +import { MiddlewareRoute } from "@medusajs/medusa"; +import { CreateQuote } from "./validators"; +import { validateAndTransformBody } from "@medusajs/framework/http"; + +export const storeQuotesMiddlewares: MiddlewareRoute[] = [ + { + method: ["POST"], + matcher: "/store/customers/me/quotes", + middlewares: [ + validateAndTransformBody(CreateQuote), + ], + }, +] +``` + +You export an array of middleware route objects, each indicating a route and the applied middlewares on it. The object can have one of 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. + +You'll now import the middleware in the `src/api/middlewares.ts` file. Create the file `src/api/middlewares.ts` with the following content: + +```ts title="src/api/middlewares.ts" +import { defineMiddlewares } from "@medusajs/framework/http"; +import { storeQuotesMiddlewares } from "./store/middlewares"; + +export default defineMiddlewares({ + routes: [ + ...storeQuotesMiddlewares + ] +}) +``` + +To export the middlewares in the `src/api/middlewares.ts` file, you use the `defineMiddlewares` function. It accepts an array of middleware route objects, which you import from the store middlewares file. + +Moving forward, any middleware you only need to add middlewares to `src/api/store/middlewares.ts`. + +### 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. + +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 retrieveQuoteTransformQueryConfig = { + defaults: quoteFields, + isList: false, +}; + +export const listQuotesTransformQueryConfig = { + defaults: quoteFields, + isList: true, +}; +``` + +You export two objects: + +- `retrieveQuoteTransformQueryConfig`: Specifies the default fields to retrieve for a single quote. +- `listQuotesTransformQueryConfig`: 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. + +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; +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. + +Finally, you'll apply these query configurations in a middleware. So, add the following middleware in `src/api/store/middlewares.ts` to the list of middlewares applied on `/store/customers/me/quotes`: + +```ts title="src/api/store/middlewares.ts" +// other imports... +import { + listQuotesTransformQueryConfig, + retrieveQuoteTransformQueryConfig, +} from "./customers/me/quotes/query-config"; +import { validateAndTransformQuery } from "@medusajs/framework/http"; + +export const storeQuotesMiddlewares: MiddlewareRoute[] = [ + { + method: ["POST"], + matcher: "/store/customers/me/quotes", + middlewares: [ + // ... + validateAndTransformQuery( + GetQuoteParams, + retrieveQuoteTransformQueryConfig + ), + ], + }, +] +``` + +The `validateAndTransformQuery` middleware that Medusa provides accepts two parameters: + +1. A Zod schema that specifies how to validate the query parameters of incoming requests. +2. A query configuration object that specifies the default fields to retrieve in the response, which you defined in the `query-config.ts` file. + +The create quote route is now ready to be used by clients to create quotes for customers. + +### Test the API Route + +To test out the API route, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier. + +#### Retrieve Publishable API Key + +All requests sent to routes starting with `/store` must have a publishable API key in their header. This ensures that the request is scoped to a specific sales channel of your storefront. + + + +To learn more about publishable API keys, refer to the [Publishable API Key documentation](../../../commerce-modules/sales-channel/publishable-api-keys/page.mdx). + + + +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 register a customer, then retrieve their authentication token to use in the request. + +Before registering 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. +- 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 Customer 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 need the ID of a product variant to add 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 previously. + +This adds the product variant to the cart. You can now use the cart to create a quote. + +#### Create Quote + +To create a quote for the customer, send a request to the `/store/customers/me/quotes` route you created: + +```bash +curl -X POST 'http://localhost:9000/store/customers/me/quotes' \ +-H 'Authorization: Bearer {token}' \ +-H 'Content-Type: application/json' \ +-H 'x-publishable-api-key: {your_publishable_api_key}' \ +--data-raw '{ + "cart_id": "{cart_id}" +}' +``` + +Make sure to replace: + +- `{token}` with the authentication token you retrieved previously. +- `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin. +- `{cart_id}` with the ID of the customer's cart. + +This will create a quote for the customer and you'll receive its details in the response. + +--- + +## Step 6: List Quotes API Route + +After the customer creates a quote, the admin user needs to view these quotes to manage them. In this step, you'll create the API route to list quotes for the admin user. Then, in the next step, you'll customize the Medusa Admin dashboard to display these quotes. + +The process of creating this API route will be somewhat similar to the previous route you created. You'll create the route, define the query configurations, and apply them in a middleware. + +### Implement API Route + +To create the API route, create the file `src/api/admin/quotes/route.ts` with the following content: + +![Directory structure after adding the admin quotes route](https://res.cloudinary.com/dza7lstvk/image/upload/v1741094735/Medusa%20Resources/quote-18_uvwqt6.jpg) + +```ts title="src/api/admin/quotes/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"; +import { ContainerRegistrationKeys } from "@medusajs/framework/utils"; + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY); + + const { data: quotes, metadata } = await query.graph({ + entity: "quote", + ...req.queryConfig, + }); + + res.json({ + quotes, + count: metadata!.count, + offset: metadata!.skip, + limit: metadata!.take, + }); +}; +``` + +You export a `GET` function in this file, which exposes a `GET` API route at `/admin/quotes`. + +In the route handler function, you resolve Query from the Medusa container and use it to retrieve the list of quotes. Similar to before, you use `req.queryConfig` to specify the fields to retrieve in the response. + +`req.queryConfig` also includes pagination parameters, such as `limit`, `offset`, and `count`, and they're returned in the `metadata` property of Query's result. You return the pagination details and the list of quotes in the response. + + + +Learn more about paginating Query results in the [Query documentation](!docs!/learn/fundamentals/module-links/query#apply-pagination). + + + +### 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 retrieveQuoteTransformQueryConfig = { + defaults: quoteFields, + isList: false, +}; + +export const listQuotesTransformQueryConfig = { + defaults: quoteFields, + isList: true, +}; +``` + +You export two objects: `retrieveQuoteTransformQueryConfig` and `listQuotesTransformQueryConfig`, which specify the default fields to retrieve for a single quote and a list of quotes, respectively. + +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"; +import { z } from "zod"; + +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. + +Finally, you need to apply the `validateAndTransformQuery` middleware on this route. Similar to what you did with the store route, you'll create a middlewares file for admin route, then import it in `src/api/middlewares.ts`. + +Create the file `src/api/admin/middlewares.ts` with the following content: + +![Directory structure after adding the admin middlewares file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741095962/Medusa%20Resources/quote-21_wgss4u.jpg) + +```ts title="src/api/admin/middlewares.ts" +import { validateAndTransformQuery } from "@medusajs/framework"; +import { MiddlewareRoute } from "@medusajs/medusa"; +import { AdminGetQuoteParams } from "./validators"; +import { listQuotesTransformQueryConfig } from "./query-config"; + +export const adminQuotesMiddlewares: MiddlewareRoute[] = [ + { + method: ["GET"], + matcher: "/admin/quotes*", + middlewares: [ + validateAndTransformQuery( + AdminGetQuoteParams, + listQuotesTransformQueryConfig + ), + ], + }, +] +``` + +You export an array of middleware route objects, with one middleware applied to routes starting with `/admin/quotes` when a `GET` request is received. The middleware applies the `validateAndTransformQuery` middleware, which validates the query parameters and sets the Query configurations based on the defaults you defined and the passed query parameters. + +Lastly, import the `adminQuotesMiddlewares` in `src/api/middlewares.ts` to apply it to the admin routes: + +```ts title="src/api/middlewares.ts" +// other imports... +import { adminQuotesMiddlewares } from "./admin/quotes/middlewares"; + +export default defineMiddlewares({ + routes: [ + // ... + ...adminQuotesMiddlewares, + ] +}) +``` + +You can now add admin middlewares in the `src/api/admin/middlewares.ts` file, and they'll be applied as expected. + +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: Add 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. + +### 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 qoutes. +- `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, 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(`/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, it uses the JS SDK's `client.fetch` method to retrieve the quotes from the `/admin/quotes` route. + +The hook returns 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 configuation 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 componet requires you first define the columns of the table. + +Add in the same file and before the `Quotes` component the following: + +```tsx title="src/admin/routes/quotes/page.tsx" +const StatusTitles: Record = { + accepted: "Accepted", + customer_rejected: "Customer Rejected", + merchant_rejected: "Merchant Rejected", + pending_merchant: "Pending Merchant", + pending_customer: "Pending Customer", +}; + +const columnHelper = createDataTableColumnHelper() + +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({ + 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 ( + <> + + + Quotes + + + + + Products + + + + + + + + ); +}; +``` + +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` configurtions you naviagte 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. + +![Quotes table in Medusa Admin](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 manage a 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 }, + ...req.queryConfig, + }, + { 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 specified fields in the query parameter. + + + +Since you applied the middleware earlier to the `/admin/quotes*` route pattern, it will automatically apply to this route as well. + + + +You'll test this route in the next step as you create the UI route for a single quote. + +--- + +## Step 9: Single Quote 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. + +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 single quote using the API route you added in the previous step. + +In `src/api/admin/hooks/quote.tsx`, add the following: + +```tsx title="src/api/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(`/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. + +### Create Amount Component + +In the quote's UI route, you want to display changes in amounts for items and totals. This is useful as you later add the capability to edit an item's price and quantity. + +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 ( +
+ - +
+ ) + } + + const formatted = formatAmount(amount, currencyCode); + const originalAmountPresent = typeof originalAmount === "number"; + const originalAmountDiffers = originalAmount !== amount; + const shouldShowAmountDiff = originalAmountPresent && originalAmountDiffers; + + return ( +
+ {shouldShowAmountDiff ? ( + <> + + {formatAmount(originalAmount!, currencyCode)} + + {formatted} + + ) : ( + <> + {formatted} + + )} +
+ ); +}; +``` + +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 when you want to display any amount. + + +### 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. + +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 ( +
+
+
+ + {item.title} + + + {item.variant_sku && ( +
+ {item.variant_sku} +
+ )} + + {item.variant?.options?.map((o) => o.value).join(" · ")} + +
+
+ +
+
+ +
+ +
+
+ + {item.quantity}x + +
+ +
+ + {isItemUpdated && ( + + Modified + + )} +
+ +
+
+ + +
+
+ ); +}; +``` + +You first define the component for a single 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 ( +
+ {preview.items?.map((item) => { + return ( + + ); + })} +
+ ); +}; +``` + +In this component, you loop over the order's items and show each of them using the `QuoteItem` component. +