From 55a1f232a3746a22adb1fcd1844b2659077a59f9 Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Tue, 14 Mar 2023 10:35:59 +0100 Subject: [PATCH] Feat(admin-ui, medusa): request return with location (#3451) * add location_id to request_return endpoint to support "receive_now" returns * changeset * admin request return * add locations to recieving returns * cleanup test * add check for inventory service --- .changeset/weak-hats-develop.md | 6 + .../__tests__/inventory/order/order.js | 59 ++++++++ .../src/components/molecules/select/index.tsx | 2 +- .../orders/details/receive-return/index.tsx | 141 ++++++++++++++---- .../domain/orders/details/returns/index.tsx | 71 ++++++++- .../api/routes/admin/orders/request-return.ts | 15 +- packages/medusa/src/types/return.ts | 1 + 7 files changed, 256 insertions(+), 39 deletions(-) create mode 100644 .changeset/weak-hats-develop.md diff --git a/.changeset/weak-hats-develop.md b/.changeset/weak-hats-develop.md new file mode 100644 index 0000000000..08f4b73da9 --- /dev/null +++ b/.changeset/weak-hats-develop.md @@ -0,0 +1,6 @@ +--- +"@medusajs/admin-ui": patch +"@medusajs/medusa": patch +--- + +feat(medusa,admin-ui): support location_id in diff --git a/integration-tests/plugins/__tests__/inventory/order/order.js b/integration-tests/plugins/__tests__/inventory/order/order.js index 72ab304bcd..f640332cae 100644 --- a/integration-tests/plugins/__tests__/inventory/order/order.js +++ b/integration-tests/plugins/__tests__/inventory/order/order.js @@ -198,6 +198,65 @@ describe("/store/carts", () => { ) }) + it("increases stocked quantity when return is received at location", async () => { + const api = useApi() + + const fulfillmentRes = await api.post( + `/admin/orders/${order.id}/fulfillment`, + { + items: [{ item_id: lineItemId, quantity: 1 }], + location_id: locationId, + }, + adminHeaders + ) + + const shipmentRes = await api.post( + `/admin/orders/${order.id}/shipment`, + { + fulfillment_id: fulfillmentRes.data.order.fulfillments[0].id, + }, + adminHeaders + ) + + expect(shipmentRes.status).toBe(200) + + let inventoryItem = await api.get( + `/admin/inventory-items/${invItemId}`, + adminHeaders + ) + + expect(inventoryItem.data.inventory_item.location_levels[0]).toEqual( + expect.objectContaining({ + stocked_quantity: 0, + reserved_quantity: 0, + available_quantity: 0, + }) + ) + + const requestReturnRes = await api.post( + `/admin/orders/${order.id}/return`, + { + receive_now: true, + location_id: locationId, + items: [{ item_id: lineItemId, quantity: 1 }], + }, + adminHeaders + ) + + expect(requestReturnRes.status).toBe(200) + inventoryItem = await api.get( + `/admin/inventory-items/${invItemId}`, + adminHeaders + ) + expect(inventoryItem.data.inventory_item.location_levels[0]).toEqual( + expect.objectContaining({ + stocked_quantity: 1, + reserved_quantity: 0, + available_quantity: 1, + }) + ) + }) + it("adjusts inventory levels on successful fulfillment without reservation", async () => { const api = useApi() diff --git a/packages/admin-ui/ui/src/components/molecules/select/index.tsx b/packages/admin-ui/ui/src/components/molecules/select/index.tsx index c32778a8f0..6884ddf879 100644 --- a/packages/admin-ui/ui/src/components/molecules/select/index.tsx +++ b/packages/admin-ui/ui/src/components/molecules/select/index.tsx @@ -23,7 +23,7 @@ export type SelectOption = { type MultiSelectProps = InputHeaderProps & { // component props - label: string + label?: string required?: boolean name?: string className?: string diff --git a/packages/admin-ui/ui/src/domain/orders/details/receive-return/index.tsx b/packages/admin-ui/ui/src/domain/orders/details/receive-return/index.tsx index 2ef99f7e8e..ecce3f06bd 100644 --- a/packages/admin-ui/ui/src/domain/orders/details/receive-return/index.tsx +++ b/packages/admin-ui/ui/src/domain/orders/details/receive-return/index.tsx @@ -1,10 +1,17 @@ -import { Order, Return } from "@medusajs/medusa" +import React from "react" +import { + AdminPostReturnsReturnReceiveReq, + Order, + Return, + StockLocationDTO, +} from "@medusajs/medusa" import { useAdminOrder, useAdminReceiveReturn } from "medusa-react" import { useEffect, useMemo } from "react" import { useForm } from "react-hook-form" import Button from "../../../../components/fundamentals/button" import Modal from "../../../../components/molecules/modal" import useNotification from "../../../../hooks/use-notification" +import { useFeatureFlag } from "../../../../providers/feature-flag-provider" import { getErrorMessage } from "../../../../utils/error-messages" import { nestedForm } from "../../../../utils/nested-form" import { ItemsToReceiveFormType } from "../../components/items-to-receive-form" @@ -13,6 +20,9 @@ import { RefundAmountFormType } from "../../components/refund-amount-form" import { ReceiveReturnSummary } from "../../components/rma-summaries/receive-return-summary" import { getDefaultReceiveReturnValues } from "../utils/get-default-values" import useOrdersExpandParam from "../utils/use-admin-expand-paramter" +import { useAdminStockLocations } from "medusa-react" +import Select from "../../../../components/molecules/select/next-select/select" +import Spinner from "../../../../components/atoms/spinner" type Props = { order: Order @@ -26,12 +36,53 @@ export type ReceiveReturnFormType = { } export const ReceiveReturnMenu = ({ order, returnRequest, onClose }: Props) => { + const { isFeatureEnabled } = useFeatureFlag() + const isLocationFulfillmentEnabled = + isFeatureEnabled("inventoryService") && + isFeatureEnabled("stockLocationService") + const { mutate, isLoading } = useAdminReceiveReturn(returnRequest.id) const { orderRelations } = useOrdersExpandParam() const { refetch } = useAdminOrder(order.id, { expand: orderRelations, }) + const { + stock_locations, + refetch: refetchLocations, + isLoading: isLoadingLocations, + } = useAdminStockLocations( + {}, + { + enabled: isLocationFulfillmentEnabled, + } + ) + + React.useEffect(() => { + if (isLocationFulfillmentEnabled) { + refetchLocations() + } + }, [isLocationFulfillmentEnabled, refetchLocations]) + + const [selectedLocation, setSelectedLocation] = React.useState<{ + value: string + label: string + } | null>(null) + + useEffect(() => { + if (isLocationFulfillmentEnabled && stock_locations?.length) { + const location = stock_locations.find( + (sl: StockLocationDTO) => sl.id === returnRequest.location_id + ) + if (location) { + setSelectedLocation({ + value: location.id, + label: location.name, + }) + } + } + }, [isLocationFulfillmentEnabled, stock_locations, returnRequest.location_id]) + /** * If the return was refunded as part of a refund claim, we do not allow the user to * specify a refund amount, or want to display a summary. @@ -104,36 +155,39 @@ export const ReceiveReturnMenu = ({ order, returnRequest, onClose }: Props) => { refundAmount = 0 } - mutate( - { - items: data.receive_items.items.map((i) => ({ - item_id: i.item_id, - quantity: i.quantity, - })), - refund: refundAmount, + const toCreate: AdminPostReturnsReturnReceiveReq = { + items: data.receive_items.items.map((i) => ({ + item_id: i.item_id, + quantity: i.quantity, + })), + refund: refundAmount, + } + + if (selectedLocation && isLocationFulfillmentEnabled) { + toCreate.location_id = selectedLocation.value + } + + mutate(toCreate, { + onSuccess: () => { + notification( + "Successfully received return", + `Received return for order #${order.display_id}`, + "success" + ) + + // We need to refetch the order to get the updated state + refetch() + + onClose() }, - { - onSuccess: () => { - notification( - "Successfully received return", - `Received return for order #${order.display_id}`, - "success" - ) - - // We need to refetch the order to get the updated state - refetch() - - onClose() - }, - onError: (error) => { - notification( - "Failed to receive return", - getErrorMessage(error), - "error" - ) - }, - } - ) + onError: (error) => { + notification( + "Failed to receive return", + getErrorMessage(error), + "error" + ) + }, + }) }) return ( @@ -149,6 +203,33 @@ export const ReceiveReturnMenu = ({ order, returnRequest, onClose }: Props) => { order={order} form={nestedForm(form, "receive_items")} /> + + {isLocationFulfillmentEnabled && ( +
+

Location

+

+ Choose which location you want to return the items to. +

+ {isLoadingLocations ? ( + + ) : ( + ({ + label: sl.name, + value: sl.id, + })) || [] + } + /> +
+ )} +

Shipping

+

+ Choose which shipping method you want to use for this return. +

{shippingLoading ? (
) : (