diff --git a/integration-tests/http/__tests__/exchanges/exchanges.spec.ts b/integration-tests/http/__tests__/exchanges/exchanges.spec.ts index 72663e0688..4acc27b699 100644 --- a/integration-tests/http/__tests__/exchanges/exchanges.spec.ts +++ b/integration-tests/http/__tests__/exchanges/exchanges.spec.ts @@ -427,6 +427,30 @@ medusaIntegrationTestRunner({ }, adminHeaders ) + + const { response } = await api + .post(`/admin/exchanges/${exchangeId2}/request`, {}, adminHeaders) + .catch((e) => e) + + expect(response.data).toEqual({ + type: "invalid_data", + message: + "Order exchange request should have atleast 1 item inbound and 1 item outbound", + }) + + await api.post( + `/admin/exchanges/${exchangeId2}/outbound/items`, + { + items: [ + { + variant_id: productExtra.variants[0].id, + quantity: 2, + }, + ], + }, + adminHeaders + ) + await api.post( `/admin/exchanges/${exchangeId2}/request`, {}, diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-claim/components/claim-create-form/claim-create-form.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-claim/components/claim-create-form/claim-create-form.tsx index f34fc7c659..ef049675ac 100644 --- a/packages/admin-next/dashboard/src/routes/orders/order-create-claim/components/claim-create-form/claim-create-form.tsx +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-claim/components/claim-create-form/claim-create-form.tsx @@ -36,7 +36,7 @@ import { AddClaimItemsTable } from "../add-claim-items-table" import { ClaimInboundItem } from "./claim-inbound-item.tsx" import { ClaimCreateSchema, CreateClaimSchemaType } from "./schema" -import { AdminReturn } from "@medusajs/types" +import { AdminReturn, HttpTypes } from "@medusajs/types" import { useAddClaimInboundItems, useAddClaimInboundShipping, @@ -275,7 +275,10 @@ export const ClaimCreateForm = ({ }) } } else { - append({ item_id: i.id, quantity: i.detail.return_requested_quantity }) + append( + { item_id: i.id, quantity: i.detail.return_requested_quantity }, + { shouldFocus: false } + ) } }) @@ -551,16 +554,26 @@ export const ClaimCreateForm = ({ }) } }} - onUpdate={(payload) => { - const actionId = previewItems + onUpdate={(payload: HttpTypes.AdminUpdateReturnItems) => { + const action = previewItems .find((i) => i.id === item.item_id) - ?.actions?.find((a) => a.action === "RETURN_ITEM")?.id + ?.actions?.find((a) => a.action === "RETURN_ITEM") - if (actionId) { + if (action) { updateInboundItem( - { ...payload, actionId }, + { ...payload, actionId: action.id }, { onError: (error) => { + if ( + action.details?.quantity && + payload.quantity + ) { + form.setValue( + `inbound_items.${index}.quantity`, + action.details?.quantity as number + ) + } + toast.error(error.message) }, } diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-claim/components/claim-create-form/claim-outbound-section.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-claim/components/claim-create-form/claim-outbound-section.tsx index 92277d7e8a..1aa6af043f 100644 --- a/packages/admin-next/dashboard/src/routes/orders/order-create-claim/components/claim-create-form/claim-outbound-section.tsx +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-claim/components/claim-create-form/claim-outbound-section.tsx @@ -9,6 +9,7 @@ import { useEffect, useMemo, useState } from "react" import { useFieldArray, UseFormReturn } from "react-hook-form" import { useTranslation } from "react-i18next" +import { HttpTypes } from "@medusajs/types" import { Form } from "../../../../../components/common/form" import { Combobox } from "../../../../../components/inputs/combobox" import { @@ -134,11 +135,14 @@ export const ClaimOutboundSection = ({ }) } } else { - append({ - item_id: i.id, - quantity: i.detail.quantity, - variant_id: i.variant_id, - }) + append( + { + item_id: i.id, + quantity: i.detail.quantity, + variant_id: i.variant_id, + }, + { shouldFocus: false } + ) } }) @@ -349,7 +353,7 @@ export const ClaimOutboundSection = ({ }) } }} - onUpdate={(payload) => { + onUpdate={(payload: HttpTypes.AdminUpdateReturnItems) => { const actionId = previewOutboundItems .find((i) => i.id === item.item_id) ?.actions?.find((a) => a.action === "ITEM_ADD")?.id diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-inbound-section.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-inbound-section.tsx index b2c04f561e..3d6cdcb47e 100644 --- a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-inbound-section.tsx +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-inbound-section.tsx @@ -10,6 +10,7 @@ import { useEffect, useMemo, useState } from "react" import { useFieldArray, UseFormReturn } from "react-hook-form" import { useTranslation } from "react-i18next" +import { HttpTypes } from "@medusajs/types" import { Form } from "../../../../../components/common/form" import { Combobox } from "../../../../../components/inputs/combobox" import { @@ -172,7 +173,10 @@ export const ExchangeInboundSection = ({ }) } } else { - append({ item_id: i.id, quantity: i.detail.return_requested_quantity }) + append( + { item_id: i.id, quantity: i.detail.return_requested_quantity }, + { shouldFocus: false } + ) } }) @@ -409,16 +413,23 @@ export const ExchangeInboundSection = ({ }) } }} - onUpdate={(payload) => { - const actionId = previewInboundItems + onUpdate={(payload: HttpTypes.AdminUpdateReturnItems) => { + const action = previewInboundItems .find((i) => i.id === item.item_id) - ?.actions?.find((a) => a.action === "RETURN_ITEM")?.id + ?.actions?.find((a) => a.action === "RETURN_ITEM") - if (actionId) { + if (action) { updateInboundItem( - { ...payload, actionId }, + { ...payload, actionId: action.id }, { onError: (error) => { + if (action.details?.quantity && payload.quantity) { + form.setValue( + `inbound_items.${index}.quantity`, + action.details?.quantity as number + ) + } + toast.error(error.message) }, } diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-outbound-section.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-outbound-section.tsx index 6f8d3a3b12..f3f0459c1a 100644 --- a/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-outbound-section.tsx +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-exchange/components/exchange-create-form/exchange-outbound-section.tsx @@ -134,11 +134,14 @@ export const ExchangeOutboundSection = ({ }) } } else { - append({ - item_id: i.id, - quantity: i.detail.quantity, - variant_id: i.variant_id, - }) + append( + { + item_id: i.id, + quantity: i.detail.quantity, + variant_id: i.variant_id, + }, + { shouldFocus: false } + ) } }) diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-return/components/return-create-form/return-create-form.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-return/components/return-create-form/return-create-form.tsx index 6bfa8dd4e0..daf84ae273 100644 --- a/packages/admin-next/dashboard/src/routes/orders/order-create-return/components/return-create-form/return-create-form.tsx +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-return/components/return-create-form/return-create-form.tsx @@ -1,5 +1,6 @@ -import React, { useEffect, useMemo, useState } from "react" import { zodResolver } from "@hookform/resolvers/zod" +import { PencilSquare } from "@medusajs/icons" +import { AdminOrder, InventoryLevelDTO, ReturnDTO } from "@medusajs/types" import { Alert, Button, @@ -10,10 +11,9 @@ import { Text, toast, } from "@medusajs/ui" +import { useEffect, useMemo, useState } from "react" import { useFieldArray, useForm } from "react-hook-form" import { useTranslation } from "react-i18next" -import { AdminOrder, InventoryLevelDTO, ReturnDTO } from "@medusajs/types" -import { PencilSquare } from "@medusajs/icons" import { RouteFocusModal, @@ -22,14 +22,8 @@ import { useStackedModal, } from "../../../../../components/modals" -import { ReturnCreateSchema, ReturnCreateSchemaType } from "./schema" -import { AddReturnItemsTable } from "../add-return-items-table" import { Form } from "../../../../../components/common/form" -import { ReturnItem } from "./return-item" import { Combobox } from "../../../../../components/inputs/combobox" -import { useStockLocations } from "../../../../../hooks/api/stock-locations" -import { useShippingOptions } from "../../../../../hooks/api/shipping-options" -import { getStylizedAmount } from "../../../../../lib/money-amount-helpers" import { useAddReturnItem, useAddReturnShipping, @@ -41,8 +35,14 @@ import { useUpdateReturnItem, useUpdateReturnShipping, } from "../../../../../hooks/api/returns" -import { currencies } from "../../../../../lib/data/currencies" +import { useShippingOptions } from "../../../../../hooks/api/shipping-options" +import { useStockLocations } from "../../../../../hooks/api/stock-locations" import { sdk } from "../../../../../lib/client" +import { currencies } from "../../../../../lib/data/currencies" +import { getStylizedAmount } from "../../../../../lib/money-amount-helpers" +import { AddReturnItemsTable } from "../add-return-items-table" +import { ReturnItem } from "./return-item" +import { ReturnCreateSchema, ReturnCreateSchemaType } from "./schema" type ReturnCreateFormProps = { order: AdminOrder @@ -455,12 +455,26 @@ export const ReturnCreateForm = ({ } }} onUpdate={(payload) => { - const actionId = previewItems + const action = previewItems .find((i) => i.id === item.item_id) - ?.actions?.find((a) => a.action === "RETURN_ITEM")?.id + ?.actions?.find((a) => a.action === "RETURN_ITEM") - if (actionId) { - updateReturnItem({ ...payload, actionId }) + if (action) { + updateReturnItem( + { ...payload, actionId: action.id }, + { + onError: (error) => { + if (action.details?.quantity && payload.quantity) { + form.setValue( + `items.${index}.quantity`, + action.details?.quantity as number + ) + } + + toast.error(error.message) + }, + } + ) } }} index={index} diff --git a/packages/core/core-flows/src/order/workflows/claim/confirm-claim-request.ts b/packages/core/core-flows/src/order/workflows/claim/confirm-claim-request.ts index 7d9beaadd1..ba0eccbacf 100644 --- a/packages/core/core-flows/src/order/workflows/claim/confirm-claim-request.ts +++ b/packages/core/core-flows/src/order/workflows/claim/confirm-claim-request.ts @@ -3,26 +3,29 @@ import { OrderChangeActionDTO, OrderChangeDTO, OrderClaimDTO, + OrderClaimItemDTO, OrderDTO, OrderPreviewDTO, + OrderReturnItemDTO, } from "@medusajs/types" import { ChangeActionType, + MedusaError, Modules, OrderChangeStatus, ReturnStatus, } from "@medusajs/utils" import { - WorkflowResponse, createStep, createWorkflow, parallelize, transform, when, + WorkflowResponse, } from "@medusajs/workflows-sdk" -import { createRemoteLinkStep, useRemoteQueryStep } from "../../../common" import { reserveInventoryStep } from "../../../cart/steps/reserve-inventory" import { prepareConfirmInventoryInput } from "../../../cart/utils/prepare-confirm-inventory-input" +import { createRemoteLinkStep, useRemoteQueryStep } from "../../../common" import { createReturnFulfillmentWorkflow } from "../../../fulfillment/workflows/create-return-fulfillment" import { previewOrderChangeStep, updateReturnsStep } from "../../steps" import { createOrderClaimItemsFromActionsStep } from "../../steps/claim/create-claim-items-from-actions" @@ -58,6 +61,29 @@ export const confirmClaimRequestValidationStep = createStep( } ) +/** + * This step confirms that a requested claim has atleast one item to return or send + */ +const confirmIfClaimItemsArePresent = createStep( + "confirm-if-items-are-present", + async function ({ + claimItems, + returnItems, + }: { + claimItems: OrderClaimItemDTO[] + returnItems?: OrderReturnItemDTO[] + }) { + if (claimItems.length > 0 || (returnItems && returnItems.length > 0)) { + return + } + + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Order claim request should have atleast 1 item` + ) + } +) + function prepareFulfillmentData({ order, items, @@ -188,7 +214,13 @@ export const confirmClaimRequestWorkflow = createWorkflow( ): WorkflowResponse { const orderClaim: OrderClaimDTO = useRemoteQueryStep({ entry_point: "order_claim", - fields: ["id", "status", "order_id", "canceled_at"], + fields: [ + "id", + "status", + "order_id", + "canceled_at", + "additional_items.id", + ], variables: { id: input.claim_id }, list: false, throw_if_key_not_found: true, @@ -241,12 +273,16 @@ export const confirmClaimRequestWorkflow = createWorkflow( const orderPreview = previewOrderChangeStep(order.id) - const [createClaimItems, createdReturnItems] = parallelize( + const [createdClaimItems, createdReturnItems] = parallelize( createOrderClaimItemsFromActionsStep(claimItems), - createReturnItemsFromActionsStep(returnItems), - confirmOrderChanges({ changes: [orderChange], orderId: order.id }) + createReturnItemsFromActionsStep(returnItems) ) + confirmIfClaimItemsArePresent({ + claimItems: createdClaimItems, + returnItems: createdReturnItems, + }) + const returnId = transform( { createdReturnItems }, ({ createdReturnItems }) => { @@ -254,6 +290,8 @@ export const confirmClaimRequestWorkflow = createWorkflow( } ) + confirmOrderChanges({ changes: [orderChange], orderId: order.id }) + when({ returnId }, ({ returnId }) => { return !!returnId }).then(() => { @@ -266,9 +304,12 @@ export const confirmClaimRequestWorkflow = createWorkflow( ]) }) - const claimId = transform({ createClaimItems }, ({ createClaimItems }) => { - return createClaimItems?.[0]?.claim_id - }) + const claimId = transform( + { createdClaimItems }, + ({ createdClaimItems }) => { + return createdClaimItems?.[0]?.claim_id + } + ) const { returnShippingMethod, claimShippingMethod } = transform( { orderPreview, orderClaim, returnId }, diff --git a/packages/core/core-flows/src/order/workflows/exchange/confirm-exchange-request.ts b/packages/core/core-flows/src/order/workflows/exchange/confirm-exchange-request.ts index abda03d091..69c56131a7 100644 --- a/packages/core/core-flows/src/order/workflows/exchange/confirm-exchange-request.ts +++ b/packages/core/core-flows/src/order/workflows/exchange/confirm-exchange-request.ts @@ -4,10 +4,13 @@ import { OrderChangeDTO, OrderDTO, OrderExchangeDTO, + OrderExchangeItemDTO, OrderPreviewDTO, + OrderReturnItemDTO, } from "@medusajs/types" import { ChangeActionType, + MedusaError, Modules, OrderChangeStatus, ReturnStatus, @@ -20,9 +23,9 @@ import { transform, when, } from "@medusajs/workflows-sdk" -import { createRemoteLinkStep, useRemoteQueryStep } from "../../../common" import { reserveInventoryStep } from "../../../cart/steps/reserve-inventory" import { prepareConfirmInventoryInput } from "../../../cart/utils/prepare-confirm-inventory-input" +import { createRemoteLinkStep, useRemoteQueryStep } from "../../../common" import { createReturnFulfillmentWorkflow } from "../../../fulfillment/workflows/create-return-fulfillment" import { previewOrderChangeStep, updateReturnsStep } from "../../steps" import { confirmOrderChanges } from "../../steps/confirm-order-changes" @@ -58,6 +61,29 @@ export const confirmExchangeRequestValidationStep = createStep( } ) +/** + * This step confirms that a requested exchange has atleast one item to return or send + */ +const confirmIfExchangeItemsArePresent = createStep( + "confirm-if-exchange-items-are-present", + async function ({ + exchangeItems, + returnItems, + }: { + exchangeItems: OrderExchangeItemDTO[] + returnItems?: OrderReturnItemDTO[] + }) { + if (exchangeItems.length > 0 && (returnItems || []).length > 0) { + return + } + + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Order exchange request should have atleast 1 item inbound and 1 item outbound` + ) + } +) + function prepareFulfillmentData({ order, items, @@ -235,12 +261,18 @@ export const confirmExchangeRequestWorkflow = createWorkflow( const orderPreview = previewOrderChangeStep(order.id) - const [createExchangeItems, createdReturnItems] = parallelize( + const [createdExchangeItems, createdReturnItems] = parallelize( createOrderExchangeItemsFromActionsStep(exchangeItems), - createReturnItemsFromActionsStep(returnItems), - confirmOrderChanges({ changes: [orderChange], orderId: order.id }) + createReturnItemsFromActionsStep(returnItems) ) + confirmIfExchangeItemsArePresent({ + exchangeItems: createdExchangeItems, + returnItems: createdReturnItems, + }) + + confirmOrderChanges({ changes: [orderChange], orderId: order.id }) + const returnId = transform( { createdReturnItems }, ({ createdReturnItems }) => { @@ -261,9 +293,9 @@ export const confirmExchangeRequestWorkflow = createWorkflow( }) const exchangeId = transform( - { createExchangeItems }, - ({ createExchangeItems }) => { - return createExchangeItems?.[0]?.exchange_id + { createdExchangeItems }, + ({ createdExchangeItems }) => { + return createdExchangeItems?.[0]?.exchange_id } ) diff --git a/packages/core/core-flows/src/order/workflows/return/confirm-return-request.ts b/packages/core/core-flows/src/order/workflows/return/confirm-return-request.ts index 7a884cdb79..a32ec3ef65 100644 --- a/packages/core/core-flows/src/order/workflows/return/confirm-return-request.ts +++ b/packages/core/core-flows/src/order/workflows/return/confirm-return-request.ts @@ -3,10 +3,12 @@ import { OrderChangeDTO, OrderDTO, OrderPreviewDTO, + OrderReturnItemDTO, ReturnDTO, } from "@medusajs/types" import { ChangeActionType, + MedusaError, Modules, OrderChangeStatus, ReturnStatus, @@ -54,6 +56,23 @@ export const confirmReturnRequestValidationStep = createStep( } ) +/** + * This step confirms that a requested return has atleast one item + */ +const confirmIfReturnItemsArePresent = createStep( + "confirm-if-return-items-are-present", + async function ({ returnItems }: { returnItems: OrderReturnItemDTO[] }) { + if (returnItems.length > 0) { + return + } + + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Order return request should have atleast 1 item` + ) + } +) + function prepareFulfillmentData({ order, items, @@ -203,6 +222,8 @@ export const confirmReturnRequestWorkflow = createWorkflow( changes: returnItemActions, }) + confirmIfReturnItemsArePresent({ returnItems: createdReturnItems }) + const returnShippingOptionId = transform( { orderPreview, orderReturn }, extractReturnShippingOptionId