From c4c2231a31d72210c01e2c82f151ecd9c2dda2e6 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Wed, 24 Sep 2025 12:06:26 +0300 Subject: [PATCH] docs: create returns from storefront tutorial (#13522) * docs: create returns from storefront tutorial * small change * generate llms * add location ID --- www/apps/book/public/llms-full.txt | 1270 +++++++++++++++ .../guides/storefront-returns/page.mdx | 1358 +++++++++++++++++ www/apps/resources/generated/edit-dates.mjs | 1 + www/apps/resources/generated/files-map.mjs | 4 + .../generated-commerce-modules-sidebar.mjs | 8 + ...nerated-storefront-development-sidebar.mjs | 8 + .../generated/generated-tools-sidebar.mjs | 24 +- www/apps/resources/sidebars/storefront.mjs | 5 + www/apps/resources/sidebars/tools.mjs | 17 +- www/packages/tags/src/tags/example.ts | 4 + www/packages/tags/src/tags/order.ts | 4 + www/packages/tags/src/tags/storefront.ts | 4 + 12 files changed, 2694 insertions(+), 13 deletions(-) create mode 100644 www/apps/resources/app/nextjs-starter/guides/storefront-returns/page.mdx diff --git a/www/apps/book/public/llms-full.txt b/www/apps/book/public/llms-full.txt index eff33e83ff..4b3d3ac663 100644 --- a/www/apps/book/public/llms-full.txt +++ b/www/apps/book/public/llms-full.txt @@ -97391,6 +97391,1276 @@ To test out this mechanism, run the Medusa application and the Next.js Starter S Then, update a product in the Medusa application. You can see in the Next.js Starter Storefront's terminal that a request was sent to the `/api/revalidate` endpoint, meaning that the cache was revalidated successfully. +# Create Order Returns in the Storefront + +In this tutorial, you'll learn how to let customers create order returns directly from your storefront. + +Medusa supports automated Return Merchandise Authorization (RMA) flows for orders. Customers can create return requests for their orders, and merchants can manage these requests through the Medusa Admin dashboard. Medusa provides the necessary API routes and workflows to handle returns efficiently. + +## Summary + +In this tutorial, you'll customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to let customers create return requests for their orders directly from the storefront. + +You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer. + +![Request return page in storefront](https://res.cloudinary.com/dza7lstvk/image/upload/v1758009041/Medusa%20Resources/CleanShot_2025-09-16_at_10.50.27_2x_g4rwjr.png) + +[Example Repository](https://github.com/medusajs/examples/tree/main/returns-storefront): Find the full code of the guide in this repository. + +*** + +## Step 1: Install a Medusa Application + +### Prerequisites + +- [Node.js v20+](https://nodejs.org/en/download) +- [Git CLI tool](https://git-scm.com/downloads) +- [PostgreSQL](https://www.postgresql.org/download/) + +Start by installing the Medusa application on your machine with the following command: + +```bash +npx create-medusa-app@latest +``` + +You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes. + +Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name. + +The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md). + +Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard. + +Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help. + +*** + +## Step 2: Add Return Server Functions + +In this step, you'll add server functions to the Next.js Starter Storefront that let you send requests to the Medusa application for order returns. You'll use these functions later in the storefront pages. + +If you installed the Next.js Starter Storefront with the Medusa backend, the storefront was installed in a separate directory. The directory's name is `{your-project}-storefront`. + +So, if your Medusa application's directory is `medusa-returns`, you can find the storefront by going back to the parent directory and changing to the `medusa-returns-storefront` directory: + +```bash +cd ../medusa-returns-storefront # change based on your project name +``` + +### List Return Reasons Function + +The first function sends a request to the [List Return Reasons](https://docs.medusajs.com/api/store#return-reasons_getreturnreasons) API route. It fetches the available return reasons, which customers can select from when creating a return request. + +Create the file `src/lib/data/returns.ts` with the following content: + +```ts title="src/lib/data/returns.ts" +"use server" + +import { sdk } from "@lib/config" +import { getAuthHeaders, getCacheOptions } from "@lib/data/cookies" +import medusaError from "@lib/util/medusa-error" +import { HttpTypes } from "@medusajs/types" + +export const listReturnReasons = async () => { + const headers = { + ...(await getAuthHeaders()), + } + + const next = { + ...(await getCacheOptions("return-reasons")), + } + + return sdk.client + .fetch(`/store/return-reasons`, { + method: "GET", + headers, + next, + cache: "force-cache", + }) + .then(({ return_reasons }) => return_reasons) + .catch((err) => medusaError(err)) +} +``` + +You add the `listReturnReasons` function. It sends a `GET` request to the `/store/return-reasons` API route. The function returns the list of return reasons. + +### List Return Shipping Options Function + +Next, you'll add a function that sends a request to the [List Shipping Options](https://docs.medusajs.com/api/store#shipping-options_getshippingoptions) API route. It fetches the available shipping options for returns, which customers can select from when creating a return request. + +In the same `src/lib/data/returns.ts` file, add the following function: + +```ts title="src/lib/data/returns.ts" +export const listReturnShippingOptions = async (cartId: string) => { + const headers = { + ...(await getAuthHeaders()), + } + + const next = { + ...(await getCacheOptions("shipping-options")), + } + + return sdk.client + .fetch(`/store/shipping-options`, { + method: "GET", + query: { + cart_id: cartId, + is_return: true, + }, + headers, + next, + cache: "force-cache", + }) + .then(({ shipping_options }) => shipping_options) + .catch((err) => medusaError(err)) +} +``` + +You add the `listReturnShippingOptions` function. It sends a `GET` request to the `/store/shipping-options` API route and returns the shipping options. + +The API route accepts the following query parameters: + +- `cart_id`: The ID of the cart associated with the order being returned. +- `is_return`: A boolean value set to `true` to retrieve only shipping options for returns. + +### Create Return Function + +Finally, you'll add a function that sends a request to the [Create Return](https://docs.medusajs.com/api/store#returns_postreturns) API route. This creates a return request for an order. + +In the same `src/lib/data/returns.ts` file, add the following function: + +```ts title="src/lib/data/returns.ts" +export const createReturnRequest = async ( + state: { + success: boolean + error: string | null + return: any | null + }, + formData: FormData +): Promise<{ + success: boolean + error: string | null + return: any | null +}> => { + const orderId = formData.get("order_id") as string + const items = JSON.parse(formData.get("items") as string) + const returnShippingOptionId = formData.get("return_shipping_option_id") as string + + if (!orderId || !items || !returnShippingOptionId) { + return { + success: false, + error: "Order ID, items, and return shipping option are required", + return: null, + } + } + + const headers = await getAuthHeaders() + + return await sdk.client + .fetch(`/store/returns`, { + method: "POST", + body: { + order_id: orderId, + items, + return_shipping: { + option_id: returnShippingOptionId, + }, + }, + headers, + }) + .then(({ return: returnData }) => ({ + success: true, + error: null, + return: returnData, + })) + .catch((err) => ({ + success: false, + error: err.message, + return: null, + })) +} +``` + +You add the `createReturnRequest` function. It sends a `POST` request to the `/store/returns` API route. The function accepts a `FormData` object containing the order ID, the items to be returned, and the selected return shipping option ID. + +The function returns an object with the following properties: + +- `success`: Whether the return request was created successfully. +- `error`: The error message if the request failed, or `null` if it succeeded. +- `return`: The created return object if the request succeeded, or `null` if it failed. + +*** + +## Step 3: Add Return Utilities + +In this step, you'll add a file with utility functions that will mainly help you determine whether an order and its items are eligible for return. + +An item is eligible for return if it has been delivered and not yet returned. An order is eligible for return if it has at least one item that's eligible for return. + +Create the file `src/lib/util/returns.ts` with the following content: + +```ts title="src/lib/util/returns.ts" highlights={returnUtilsHighlights} +import { HttpTypes } from "@medusajs/types" + +export type ItemWithDeliveryStatus = HttpTypes.StoreOrderLineItem & { + deliveredQuantity: number + returnableQuantity: number + isDelivered: boolean + isReturnable: boolean +} + +export const calculateReturnableQuantity = (item: HttpTypes.StoreOrderLineItem): number => { + const deliveredQuantity = item.detail?.delivered_quantity || 0 + const returnRequestedQuantity = item.detail?.return_requested_quantity || 0 + const returnReceivedQuantity = item.detail?.return_received_quantity || 0 + const writtenOffQuantity = item.detail?.written_off_quantity || 0 + + return Math.max( + 0, + deliveredQuantity - returnRequestedQuantity - returnReceivedQuantity - writtenOffQuantity + ) +} + +export const isItemReturnable = (item: HttpTypes.StoreOrderLineItem): boolean => { + return calculateReturnableQuantity(item) > 0 +} + +export const hasReturnableItems = (order: HttpTypes.StoreOrder): boolean => { + return order.items?.some(isItemReturnable) || false +} + +export const enhanceItemsWithReturnStatus = (items: HttpTypes.StoreOrderLineItem[]): ItemWithDeliveryStatus[] => { + return items.map((item) => { + const deliveredQuantity = item.detail?.delivered_quantity || 0 + const returnableQuantity = calculateReturnableQuantity(item) + + return { + ...item, + deliveredQuantity, + returnableQuantity, + isDelivered: deliveredQuantity > 0, + isReturnable: returnableQuantity > 0, + } + }) +} +``` + +You add the following utility functions: + +- `calculateReturnableQuantity`: Calculates the returnable quantity for a given item. It subtracts quantities that have been returned, requested for return, or written off from the delivered quantity. +- `isItemReturnable`: Determines if a given item is returnable by checking if its returnable quantity is greater than zero. +- `hasReturnableItems`: Checks if an order has at least one item that is returnable. +- `enhanceItemsWithReturnStatus`: Adds return status information to each item in the order. It adds the following properties: + - `deliveredQuantity`: The quantity of the item that has been delivered. + - `returnableQuantity`: The quantity of the item that is returnable. + - `isDelivered`: A boolean indicating whether any quantity of the item has been delivered. + - `isReturnable`: A boolean indicating whether any quantity of the item is returnable. + +You'll use these utility functions later in the storefront pages. They help determine whether to allow customers to create a return request for their order. They also help determine which items can be returned. + +*** + +## Step 4: Add Return Item Selector + +In this step, you'll add a component that lets customers select the quantity to return of items from their order. Later, you'll use this component in the return request page. + +![Preview of the return item selector on the request return page](https://res.cloudinary.com/dza7lstvk/image/upload/v1758008962/Medusa%20Resources/CleanShot_2025-09-16_at_10.48.41_2x_myun8q.png) + +The component displays each item's details. It lets customers specify the quantity to return based on the returnable quantity of the item. It also lets customers select a return reason for the item and provide an optional note. + +To create the component, create the file `src/modules/account/components/return-item-selector/index.tsx` with the following content: + +```tsx title="src/modules/account/components/return-item-selector/index.tsx" highlights={returnItemSelectorHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" +"use client" + +import { HttpTypes } from "@medusajs/types" +import { Badge, IconButton, Select, Textarea } from "@medusajs/ui" +import { Minus, Plus } from "@medusajs/icons" +import Thumbnail from "@modules/products/components/thumbnail" +import { convertToLocale } from "@lib/util/money" +import { ItemWithDeliveryStatus } from "../../../../lib/util/returns" + +export type ReturnItemSelection = { + id: string + quantity: number + return_reason_id?: string + note?: string +} + +type ReturnItemSelectorProps = { + items: ItemWithDeliveryStatus[] + returnReasons: HttpTypes.StoreReturnReason[] + onItemSelectionChange: (item: ReturnItemSelection) => void + selectedItems: ReturnItemSelection[] +} + +const ReturnItemSelector: React.FC = ({ + items, + returnReasons, + onItemSelectionChange, + selectedItems, +}) => { + const handleQuantityChange = ({ + item_id, + quantity, + selected_item, + }: { + item_id: string + quantity: number + selected_item?: ReturnItemSelection + }) => { + const item = items.find((i) => i.id === item_id) + if (!item || !item.isReturnable) {return} + + const maxQuantity = item.returnableQuantity + const newQuantity = Math.max(0, Math.min(quantity, maxQuantity)) + + onItemSelectionChange({ + id: item_id, + quantity: newQuantity, + return_reason_id: selected_item?.return_reason_id || "", + note: selected_item?.note || "", + }) + } + + const handleReturnReasonChange = ({ + item_id, + return_reason_id, + selected_item, + }: { + item_id: string + return_reason_id: string + selected_item?: ReturnItemSelection + }) => { + onItemSelectionChange({ + id: item_id, + quantity: selected_item?.quantity || 0, + return_reason_id, + note: selected_item?.note || "", + }) + } + + const handleNoteChange = ({ + item_id, + note, + selected_item, + }: { + item_id: string + note: string + selected_item?: ReturnItemSelection + }) => { + onItemSelectionChange({ + id: item_id, + quantity: selected_item?.quantity || 0, + return_reason_id: selected_item?.return_reason_id || "", + note, + }) + } + + // TODO render component +} + +export default ReturnItemSelector +``` + +You create the `ReturnItemSelector` component that accepts the following props: + +- `items`: The list of items in the order, enhanced with their delivery and return status. +- `returnReasons`: The list of available return reasons. +- `onItemSelectionChange`: A callback function that is called when the customer selects or updates an item to return. +- `selectedItems`: The list of items that the customer has selected to return. + +In the component, you define three functions: + +- `handleQuantityChange`: Called when the customer changes the quantity to return for an item. +- `handleReturnReasonChange`: Called when the customer selects a return reason for an item. +- `handleNoteChange`: Called when the customer adds or updates a note for an item. + +Next, you'll add a `return` statement that renders the item selector. Replace the `TODO` in the `ReturnItemSelector` component with the following: + +```tsx title="src/modules/account/components/return-item-selector/index.tsx" +return ( +
+ {items.map((item) => { + const itemSelection = selectedItems.find((si) => si.id === item.id) + const currentQuantity = itemSelection?.quantity || 0 + const currentReturnReason = itemSelection?.return_reason_id || "" + const currentNote = itemSelection?.note || "" + + return ( +
+
+
+
+ +
+
+ +
+
+

+ {item.title} +

+ {!item.isReturnable && ( + // @ts-ignore + + {!item.isDelivered ? "Not delivered" : "Not returnable"} + + )} +
+ {item.variant && ( +

+ {item.variant.title} +

+ )} +

+ {item.isReturnable ? ( + <>Available to return: {item.returnableQuantity} {item.returnableQuantity === 1 ? "item" : "items"} + ) : item.isDelivered ? ( + <>Delivered: {item.deliveredQuantity} of {item.quantity} {item.quantity === 1 ? "item" : "items"} (already processed) + ) : ( + <>Delivered: 0 of {item.quantity} {item.quantity === 1 ? "item" : "items"} + )} +

+
+ +
+ + {convertToLocale({ + amount: item.unit_price, + currency_code: "USD", // Default currency, should be passed from parent + })} + + + {item.isReturnable ? ( +
+ {/* @ts-ignore */} + handleQuantityChange({ + item_id: item.id, + quantity: currentQuantity - 1, + selected_item: itemSelection, + })} + disabled={currentQuantity <= 0} + > + + + + + {currentQuantity} + + + {/* @ts-ignore */} + handleQuantityChange({ + item_id: item.id, + quantity: currentQuantity + 1, + selected_item: itemSelection, + })} + disabled={currentQuantity >= item.returnableQuantity} + > + + +
+ ) : ( +
+ Not available +
+ )} +
+
+ + {item.isReturnable && currentQuantity > 0 && ( +
+
+ + +
+ +
+ +