diff --git a/.changeset/shaggy-ghosts-crash.md b/.changeset/shaggy-ghosts-crash.md new file mode 100644 index 0000000000..6011f627bc --- /dev/null +++ b/.changeset/shaggy-ghosts-crash.md @@ -0,0 +1,6 @@ +--- +"@medusajs/admin-ui": patch +"@medusajs/medusa": patch +--- + +feat(medusa,admin): location_ids in swap and claim creation diff --git a/integration-tests/plugins/__tests__/inventory/order/order.js b/integration-tests/plugins/__tests__/inventory/order/order.js index 04f194cfce..5f1bc676d7 100644 --- a/integration-tests/plugins/__tests__/inventory/order/order.js +++ b/integration-tests/plugins/__tests__/inventory/order/order.js @@ -15,6 +15,7 @@ const { simpleOrderFactory, simpleRegionFactory, simpleCartFactory, + simpleShippingOptionFactory, } = require("../../../factories") jest.setTimeout(30000) @@ -62,9 +63,11 @@ describe("/store/carts", () => { let variantId let prodVarInventoryService let inventoryService + let lineItemService let stockLocationService let salesChannelLocationService let regionId + let orderId beforeEach(async () => { const api = useApi() @@ -72,6 +75,7 @@ describe("/store/carts", () => { prodVarInventoryService = appContainer.resolve( "productVariantInventoryService" ) + lineItemService = appContainer.resolve("lineItemService") inventoryService = appContainer.resolve("inventoryService") stockLocationService = appContainer.resolve("stockLocationService") salesChannelLocationService = appContainer.resolve( @@ -115,7 +119,7 @@ describe("/store/carts", () => { stocked_quantity: 100, }) - const { id: orderId } = await simpleOrderFactory(dbConnection, { + const { id: order_id } = await simpleOrderFactory(dbConnection, { sales_channel: "test-channel", line_items: [ { @@ -124,6 +128,8 @@ describe("/store/carts", () => { id: "line-item-id", }, ], + payment_status: "captured", + fulfillment_status: "fulfilled", shipping_methods: [ { shipping_option: { @@ -133,9 +139,17 @@ describe("/store/carts", () => { ], }) - const orderRes = await api.get(`/admin/orders/${orderId}`, adminHeaders) + orderId = order_id + const orderRes = await api.get(`/admin/orders/${order_id}`, adminHeaders) order = orderRes.data.order + await simpleShippingOptionFactory(dbConnection, { + id: "test-return-option", + is_return: true, + region_id: regionId, + price: 0, + }) + const inventoryItem = await api.get( `/admin/inventory-items/${invItem.id}`, adminHeaders @@ -150,6 +164,63 @@ describe("/store/carts", () => { ) }) + describe("swaps", () => { + it("adjusts reservations on successful swap", async () => { + const api = useApi() + + const response = await api.post( + `/admin/orders/${orderId}/swaps`, + { + return_items: [ + { + item_id: "line-item-id", + quantity: 1, + }, + ], + return_location_id: locationId, + }, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.order.swaps[0].return_order.location_id).toEqual( + locationId + ) + }) + }) + + describe("claims", () => { + it("adjusts reservations on successful swap", async () => { + const api = useApi() + + await lineItemService.update("line-item-id", { + fulfilled_quantity: 1, + }) + + const response = await api.post( + `/admin/orders/${orderId}/claims`, + { + type: "refund", + claim_items: [ + { + item_id: "line-item-id", + quantity: 1, + reason: "production_failure", + }, + ], + return_shipping: { option_id: "test-return-option", price: 0 }, + return_location_id: locationId, + }, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.order.claims[0].return_order.location_id).toEqual( + locationId + ) + }) + }) + describe("Fulfillments", () => { const lineItemId = "line-item-id" diff --git a/integration-tests/plugins/factories/simple-shipping-option-factory.ts b/integration-tests/plugins/factories/simple-shipping-option-factory.ts index 1df50b3176..78ae470f0e 100644 --- a/integration-tests/plugins/factories/simple-shipping-option-factory.ts +++ b/integration-tests/plugins/factories/simple-shipping-option-factory.ts @@ -3,6 +3,7 @@ import faker from "faker" import { ShippingOption, ShippingOptionPriceType, ShippingProfile, ShippingProfileType, } from "@medusajs/medusa" export type ShippingOptionFactoryData = { + id?: string name?: string region_id: string is_return?: boolean @@ -29,7 +30,7 @@ export const simpleShippingOptionFactory = async ( }) const created = manager.create(ShippingOption, { - id: `simple-so-${Math.random() * 1000}`, + id: data.id || `simple-so-${Math.random() * 1000}`, name: data.name || "Test Method", is_return: data.is_return ?? false, region_id: data.region_id, diff --git a/packages/admin-ui/ui/src/domain/orders/details/claim/register-claim-menu.tsx b/packages/admin-ui/ui/src/domain/orders/details/claim/register-claim-menu.tsx index 96333c2a08..2045335881 100644 --- a/packages/admin-ui/ui/src/domain/orders/details/claim/register-claim-menu.tsx +++ b/packages/admin-ui/ui/src/domain/orders/details/claim/register-claim-menu.tsx @@ -1,15 +1,18 @@ -import { ClaimReason, Order } from "@medusajs/medusa" -import { useAdminCreateClaim } from "medusa-react" +import { ClaimReason, Order, StockLocationDTO } from "@medusajs/medusa" +import { useAdminStockLocations, useAdminCreateClaim } from "medusa-react" import { useEffect } from "react" -import { useForm, useWatch } from "react-hook-form" +import { Controller, useForm, useWatch } from "react-hook-form" +import Spinner from "../../../../components/atoms/spinner" import Button from "../../../../components/fundamentals/button" import Modal from "../../../../components/molecules/modal" import LayeredModal, { useLayeredModal, } from "../../../../components/molecules/modal/layered-modal" +import Select from "../../../../components/molecules/select/next-select/select" import { AddressPayload } from "../../../../components/templates/address-form" import useImperativeDialog from "../../../../hooks/use-imperative-dialog" 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 ClaimTypeForm, { @@ -35,6 +38,10 @@ export type CreateClaimFormType = { return_items: ItemsToReturnFormType additional_items: ItemsToSendFormType return_shipping: ShippingFormType + selected_location?: { + value: string + label: string + } replacement_shipping: ShippingFormType shipping_address: AddressPayload claim_type: ClaimTypeFormType @@ -50,6 +57,28 @@ const RegisterClaimMenu = ({ order, onClose }: Props) => { const context = useLayeredModal() const { mutate, isLoading } = useAdminCreateClaim(order.id) + const { isFeatureEnabled } = useFeatureFlag() + const isLocationFulfillmentEnabled = + isFeatureEnabled("inventoryService") && + isFeatureEnabled("stockLocationService") + + const { + stock_locations, + refetch: refetchLocations, + isLoading: isLoadingLocations, + } = useAdminStockLocations( + {}, + { + enabled: isLocationFulfillmentEnabled, + } + ) + + useEffect(() => { + if (isLocationFulfillmentEnabled) { + refetchLocations() + } + }, [isLocationFulfillmentEnabled, refetchLocations]) + const form = useForm({ defaultValues: getDefaultClaimValues(order), }) @@ -58,6 +87,7 @@ const RegisterClaimMenu = ({ order, onClose }: Props) => { reset, formState: { isDirty }, setError, + control, } = form const notification = useNotification() @@ -91,6 +121,7 @@ const RegisterClaimMenu = ({ order, onClose }: Props) => { const type = data.claim_type.type const returnShipping = data.return_shipping const refundAmount = data.refund_amount?.amount + const returnLocation = data.selected_location?.value const replacementShipping = type === "replace" && data.replacement_shipping.option @@ -184,6 +215,7 @@ const RegisterClaimMenu = ({ order, onClose }: Props) => { province: data.shipping_address.province || undefined, } : undefined, + return_location_id: returnLocation, shipping_methods: replacementShipping ? [replacementShipping] : undefined, @@ -239,6 +271,41 @@ const RegisterClaimMenu = ({ order, onClose }: Props) => { isReturn={true} isClaim={true} /> + + {isLocationFulfillmentEnabled && ( +
+

Location

+

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

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

Items to send

{itemsToAdd.length === 0 ? ( diff --git a/packages/medusa/src/api/routes/admin/orders/create-claim.ts b/packages/medusa/src/api/routes/admin/orders/create-claim.ts index 18cde2177a..4264111c85 100644 --- a/packages/medusa/src/api/routes/admin/orders/create-claim.ts +++ b/packages/medusa/src/api/routes/admin/orders/create-claim.ts @@ -437,6 +437,10 @@ export class AdminPostOrdersOrderClaimsReq { @IsOptional() no_notification?: boolean + @IsOptional() + @IsString() + return_location_id?: string + @IsObject() @IsOptional() metadata?: Record diff --git a/packages/medusa/src/api/routes/admin/orders/create-swap.ts b/packages/medusa/src/api/routes/admin/orders/create-swap.ts index 4fe82cdaa6..c5e8805144 100644 --- a/packages/medusa/src/api/routes/admin/orders/create-swap.ts +++ b/packages/medusa/src/api/routes/admin/orders/create-swap.ts @@ -164,6 +164,7 @@ export default async (req, res) => { idempotency_key: idempotencyKey.idempotency_key, no_notification: validated.no_notification, allow_backorder: validated.allow_backorder, + location_id: validated.return_location_id, } ) @@ -361,6 +362,10 @@ export class AdminPostOrdersOrderSwapsReq { @IsOptional() no_notification?: boolean + @IsOptional() + @IsString() + return_location_id?: string + @IsBoolean() @IsOptional() allow_backorder?: boolean = true diff --git a/packages/medusa/src/services/claim.ts b/packages/medusa/src/services/claim.ts index 42f382fe9b..06c0a872b2 100644 --- a/packages/medusa/src/services/claim.ts +++ b/packages/medusa/src/services/claim.ts @@ -346,6 +346,7 @@ export default class ClaimService extends TransactionBaseService { shipping_address, shipping_address_id, no_notification, + return_location_id, ...rest } = data @@ -485,6 +486,7 @@ export default class ClaimService extends TransactionBaseService { ), shipping_method: return_shipping, no_notification: evaluatedNoNotification, + location_id: return_location_id, }) } diff --git a/packages/medusa/src/services/swap.ts b/packages/medusa/src/services/swap.ts index 5fc02ff150..4bcc1f490d 100644 --- a/packages/medusa/src/services/swap.ts +++ b/packages/medusa/src/services/swap.ts @@ -309,6 +309,7 @@ class SwapService extends TransactionBaseService { no_notification?: boolean idempotency_key?: string allow_backorder?: boolean + location_id?: string } = { no_notification: undefined } ): Promise { const { no_notification, ...rest } = custom @@ -379,6 +380,7 @@ class SwapService extends TransactionBaseService { items: returnItems as OrdersReturnItem[], shipping_method: returnShipping, no_notification: evaluatedNoNotification, + location_id: custom.location_id, }) await this.eventBus_ diff --git a/packages/medusa/src/types/claim.ts b/packages/medusa/src/types/claim.ts index ada41e7389..381367080e 100644 --- a/packages/medusa/src/types/claim.ts +++ b/packages/medusa/src/types/claim.ts @@ -18,6 +18,7 @@ export type CreateClaimInput = { order: Order claim_order_id?: string shipping_address_id?: string + return_location_id?: string } type CreateClaimReturnShippingInput = {