diff --git a/integration-tests/modules/__tests__/order/workflows/return/items.spec.ts b/integration-tests/modules/__tests__/order/workflows/return/items.spec.ts new file mode 100644 index 0000000000000..bd5d12f096001 --- /dev/null +++ b/integration-tests/modules/__tests__/order/workflows/return/items.spec.ts @@ -0,0 +1,228 @@ +import { + beginReturnOrderWorkflow, + requestItemReturnWorkflow, +} from "@medusajs/core-flows" +import { IOrderModuleService, OrderDTO, ReturnDTO } from "@medusajs/types" +import { + ContainerRegistrationKeys, + ModuleRegistrationName, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { medusaIntegrationTestRunner } from "medusa-test-utils" +import { createOrderFixture, prepareDataFixtures } from "../__fixtures__" + +jest.setTimeout(50000) + +medusaIntegrationTestRunner({ + env: { MEDUSA_FF_MEDUSA_V2: true }, + testSuite: ({ getContainer }) => { + let container + + beforeAll(() => { + container = getContainer() + }) + + describe("Order change action workflows", () => { + let order: OrderDTO + let service: IOrderModuleService + let returnOrder: ReturnDTO + + beforeEach(async () => { + const fixtures = await prepareDataFixtures({ container }) + + order = await createOrderFixture({ + container, + product: fixtures.product, + location: fixtures.location, + inventoryItem: fixtures.inventoryItem, + }) + + await beginReturnOrderWorkflow(container).run({ + input: { order_id: order.id }, + throwOnError: true, + }) + + const remoteQuery = container.resolve( + ContainerRegistrationKeys.REMOTE_QUERY + ) + + const remoteQueryObject = remoteQueryObjectFromString({ + entryPoint: "return", + variables: { order_id: order.id }, + fields: ["order_id", "id", "status", "order_change_id"], + }) + + service = container.resolve(ModuleRegistrationName.ORDER) + ;[returnOrder] = await remoteQuery(remoteQueryObject) + }) + + describe("requestItemReturnWorkflow", () => { + it("should successfully add a return item to order change", async () => { + const item = order.items![0] + const { + result: [returnItem], + } = await requestItemReturnWorkflow(container).run({ + input: { + return_id: returnOrder.id, + items: [ + { + id: item.id, + quantity: 1, + internal_note: "test", + }, + ], + }, + }) + + expect(returnItem).toEqual( + expect.objectContaining({ + id: expect.any(String), + order_id: order.id, + return_id: returnOrder.id, + reference: "return", + reference_id: returnOrder.id, + details: { + reference_id: item.id, + return_id: returnOrder.id, + quantity: 1, + }, + internal_note: "test", + action: "RETURN_ITEM", + }) + ) + }) + + it("should throw an error if return does not exist", async () => { + const item = order.items![0] + const { + errors: [error], + } = await requestItemReturnWorkflow(container).run({ + input: { + return_id: "does-not-exist", + items: [ + { + id: item.id, + quantity: 1, + internal_note: "test", + }, + ], + }, + throwOnError: false, + }) + + expect(error.error.message).toEqual( + "order id not found: does-not-exist" + ) + }) + + it("should throw an error if order does not exist", async () => { + const item = order.items![0] + + await service.deleteOrders(order.id) + + const { + errors: [error], + } = await requestItemReturnWorkflow(container).run({ + input: { + return_id: returnOrder.id, + items: [ + { + id: item.id, + quantity: 1, + internal_note: "test", + }, + ], + }, + throwOnError: false, + }) + + expect(error.error.message).toEqual(`order id not found: ${order.id}`) + }) + + it("should throw an error if order change does not exist", async () => { + const item = order.items![0] + + const [orderChange] = await service.listOrderChanges( + { order_id: order.id }, + {} + ) + + await service.deleteOrderChanges(orderChange.id) + + const { + errors: [error], + } = await requestItemReturnWorkflow(container).run({ + input: { + return_id: returnOrder.id, + items: [ + { + id: item.id, + quantity: 1, + internal_note: "test", + }, + ], + }, + throwOnError: false, + }) + + expect(error.error.message).toEqual( + `An active Order Change is required to proceed` + ) + }) + + it("should throw an error if order change is not active", async () => { + const item = order.items![0] + + const [orderChange] = await service.listOrderChanges( + { order_id: order.id }, + {} + ) + + await service.cancelOrderChange(orderChange.id) + + const { + errors: [error], + } = await requestItemReturnWorkflow(container).run({ + input: { + return_id: returnOrder.id, + items: [ + { + id: item.id, + quantity: 1, + internal_note: "test", + }, + ], + }, + throwOnError: false, + }) + + expect(error.error.message).toEqual( + `Order Change cannot be modified: ${orderChange.id}.` + ) + }) + + it("should throw an error if return item is not present in the order", async () => { + const { + errors: [error], + } = await requestItemReturnWorkflow(container).run({ + input: { + return_id: returnOrder.id, + items: [ + { + id: "does-not-exist", + quantity: 1, + internal_note: "test", + }, + ], + }, + throwOnError: false, + }) + + expect(error.error.message).toEqual( + `Items with ids does-not-exist does not exist in order with id ${order.id}.` + ) + }) + }) + }) + }, +}) diff --git a/packages/core/core-flows/src/order/utils/order-validation.ts b/packages/core/core-flows/src/order/utils/order-validation.ts index d227d478cced4..a7f24e2a8a148 100644 --- a/packages/core/core-flows/src/order/utils/order-validation.ts +++ b/packages/core/core-flows/src/order/utils/order-validation.ts @@ -1,5 +1,15 @@ -import { OrderDTO, OrderWorkflow, ReturnDTO } from "@medusajs/types" -import { MedusaError, OrderStatus, arrayDifference } from "@medusajs/utils" +import { + OrderChangeDTO, + OrderDTO, + OrderWorkflow, + ReturnDTO, +} from "@medusajs/types" +import { + MedusaError, + OrderStatus, + arrayDifference, + isPresent, +} from "@medusajs/utils" export function throwIfOrderIsCancelled({ order }: { order: OrderDTO }) { if (order.status === OrderStatus.CANCELED) { @@ -44,6 +54,30 @@ export function throwIfReturnIsCancelled({ } } +export function throwIfOrderChangeIsNotActive({ + orderChange, +}: { + orderChange: OrderChangeDTO +}) { + if (!isPresent(orderChange)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `An active Order Change is required to proceed` + ) + } + + if ( + orderChange.canceled_at || + orderChange.confirmed_at || + orderChange.declined_at + ) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Order change ${orderChange?.id} is not active to be modified` + ) + } +} + export function throwIfItemsDoesNotExistsInReturn({ orderReturn, inputItems, diff --git a/packages/core/core-flows/src/order/workflows/index.ts b/packages/core/core-flows/src/order/workflows/index.ts index d0b59a91bdbab..b3321147d407c 100644 --- a/packages/core/core-flows/src/order/workflows/index.ts +++ b/packages/core/core-flows/src/order/workflows/index.ts @@ -19,5 +19,6 @@ export * from "./delete-order-change-actions" export * from "./get-order-detail" export * from "./get-orders-list" export * from "./receive-return" +export * from "./request-item-return" export * from "./update-order-change-actions" export * from "./update-tax-lines" diff --git a/packages/core/core-flows/src/order/workflows/request-item-return.ts b/packages/core/core-flows/src/order/workflows/request-item-return.ts new file mode 100644 index 0000000000000..97e494b794352 --- /dev/null +++ b/packages/core/core-flows/src/order/workflows/request-item-return.ts @@ -0,0 +1,99 @@ +import { + OrderChangeActionDTO, + OrderChangeDTO, + OrderDTO, + OrderWorkflow, + ReturnDTO, +} from "@medusajs/types" +import { ChangeActionType } from "@medusajs/utils" +import { + WorkflowData, + createStep, + createWorkflow, + transform, +} from "@medusajs/workflows-sdk" +import { useRemoteQueryStep } from "../../common" +import { createOrderChangeActionsStep } from "../steps/create-order-change-actions" +import { + throwIfItemsDoesNotExistsInOrder, + throwIfOrderChangeIsNotActive, + throwIfOrderIsCancelled, + throwIfReturnIsCancelled, +} from "../utils/order-validation" + +const validationStep = createStep( + "request-item-return-validation", + async function ({ + order, + orderChange, + orderReturn, + items, + }: { + order: OrderDTO + orderReturn: ReturnDTO + orderChange: OrderChangeDTO + items: OrderWorkflow.RequestItemReturnWorkflowInput["items"] + }) { + throwIfOrderIsCancelled({ order }) + throwIfReturnIsCancelled({ orderReturn }) + throwIfOrderChangeIsNotActive({ orderChange }) + throwIfItemsDoesNotExistsInOrder({ order, inputItems: items }) + } +) + +export const requestItemReturnWorkflowId = "request-item-return" +export const requestItemReturnWorkflow = createWorkflow( + requestItemReturnWorkflowId, + function ( + input: WorkflowData + ): WorkflowData { + const orderReturn: ReturnDTO = useRemoteQueryStep({ + entry_point: "return", + fields: ["id", "status", "refund_amount", "order_id", "items.*"], + variables: { id: input.return_id }, + list: false, + throw_if_key_not_found: true, + }) + + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: ["id", "status", "items.*"], + variables: { id: orderReturn.order_id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "order-query" }) + + const orderChange: OrderChangeDTO = useRemoteQueryStep({ + entry_point: "order_change", + fields: ["id", "status"], + variables: { order_id: orderReturn.order_id }, + list: false, + }).config({ name: "order-change-query" }) + + validationStep({ order, items: input.items, orderReturn, orderChange }) + + const orderChangeActionInput = transform( + { order, orderChange, orderReturn, items: input.items }, + ({ order, orderChange, orderReturn, items }) => { + return items.map((item) => ({ + order_change_id: orderChange.id, + order_id: order.id, + return_id: orderReturn.id, + version: orderChange.version, + action: ChangeActionType.RETURN_ITEM, + internal_note: item.internal_note, + reference: "return", + reference_id: orderReturn.id, + details: { + reference_id: item.id, + return_id: orderReturn.id, + quantity: item.quantity, + metadata: item.metadata, + }, + })) + } + ) + + return createOrderChangeActionsStep(orderChangeActionInput) + } +) diff --git a/packages/core/orchestration/src/joiner/remote-joiner.ts b/packages/core/orchestration/src/joiner/remote-joiner.ts index 19af02a210fd9..d54da54db9f04 100644 --- a/packages/core/orchestration/src/joiner/remote-joiner.ts +++ b/packages/core/orchestration/src/joiner/remote-joiner.ts @@ -434,6 +434,7 @@ export class RemoteJoiner { }) if (notFound.size > 0) { + // TODO: This should say "entryPoint" resource not found and not "serviceName" resource not found throw new MedusaError( MedusaError.Types.NOT_FOUND, `${expand.serviceConfig.serviceName} ${pkField} not found: ` + diff --git a/packages/core/types/src/order/common.ts b/packages/core/types/src/order/common.ts index 4b156b7ef96ab..deb0223d8c65e 100644 --- a/packages/core/types/src/order/common.ts +++ b/packages/core/types/src/order/common.ts @@ -1141,6 +1141,7 @@ type ReturnStatus = "requested" | "received" | "partially_received" | "canceled" export interface ReturnDTO extends Omit { status: ReturnStatus refund_amount?: BigNumberValue + order_id: string } export interface OrderClaimDTO @@ -1198,7 +1199,7 @@ export interface OrderChangeDTO { /** * The version of the order change */ - version: string + version: number /** * The type of the order change */ diff --git a/packages/core/types/src/workflow/order/create-return-order.ts b/packages/core/types/src/workflow/order/create-return-order.ts index ff0284c68cb99..18ddb25bb094e 100644 --- a/packages/core/types/src/workflow/order/create-return-order.ts +++ b/packages/core/types/src/workflow/order/create-return-order.ts @@ -1,6 +1,6 @@ import { BigNumberInput } from "../../totals" -interface CreateReturnItem { +export interface CreateReturnItem { id: string quantity: BigNumberInput internal_note?: string | null diff --git a/packages/core/types/src/workflow/order/index.ts b/packages/core/types/src/workflow/order/index.ts index 62494984291e3..d59380f4f464a 100644 --- a/packages/core/types/src/workflow/order/index.ts +++ b/packages/core/types/src/workflow/order/index.ts @@ -10,3 +10,4 @@ export * from "./create-fulfillment" export * from "./create-return-order" export * from "./create-shipment" export * from "./receive-return" +export * from "./request-item-return" diff --git a/packages/core/types/src/workflow/order/request-item-return.ts b/packages/core/types/src/workflow/order/request-item-return.ts new file mode 100644 index 0000000000000..c7be7c01dcf04 --- /dev/null +++ b/packages/core/types/src/workflow/order/request-item-return.ts @@ -0,0 +1,6 @@ +import { CreateReturnItem } from "./create-return-order" + +export interface RequestItemReturnWorkflowInput { + return_id: string + items: CreateReturnItem[] +}