From d4fe2daa57fc152a6989b2bec4e24c8bbe492be6 Mon Sep 17 00:00:00 2001 From: "Carlos R. L. Rodrigues" <37986729+carlos-r-l-rodrigues@users.noreply.github.com> Date: Wed, 17 Jul 2024 05:35:33 -0300 Subject: [PATCH] fix(core-flow): request item return reason (#8152) --- .../http/__tests__/returns/returns.spec.ts | 30 ++++ .../src/order/steps/create-return-items.ts | 14 +- .../src/order/utils/validate-return-reason.ts | 64 ++++++++ .../order/workflows/create-complete-return.ts | 69 +-------- .../workflows/return/request-item-return.ts | 32 ++-- .../return/update-request-item-return.ts | 38 +++-- packages/core/types/src/order/mutations.ts | 1 - .../src/workflow/order/create-return-order.ts | 2 +- .../src/workflow/order/request-item-return.ts | 2 + .../api/admin/return-reasons/query-config.ts | 14 +- .../api/admin/return-reasons/validators.ts | 4 +- .../src/api/admin/returns/query-config.ts | 8 +- .../src/api/admin/returns/validators.ts | 3 + .../integration-tests/__tests__/returns.ts | 22 +-- .../src/services/order-module-service.ts | 141 ------------------ 15 files changed, 189 insertions(+), 255 deletions(-) create mode 100644 packages/core/core-flows/src/order/utils/validate-return-reason.ts diff --git a/integration-tests/http/__tests__/returns/returns.spec.ts b/integration-tests/http/__tests__/returns/returns.spec.ts index 7ca16097bb..9b037b9aab 100644 --- a/integration-tests/http/__tests__/returns/returns.spec.ts +++ b/integration-tests/http/__tests__/returns/returns.spec.ts @@ -13,11 +13,24 @@ medusaIntegrationTestRunner({ let returnShippingOption let shippingProfile let fulfillmentSet + let returnReason beforeEach(async () => { const container = getContainer() await createAdminUser(dbConnection, adminHeaders, container) + returnReason = ( + await api.post( + "/admin/return-reasons", + { + value: "return-reason-test", + label: "Test return reason", + description: "This is the reason description!!!", + }, + adminHeaders + ) + ).data.return_reason + const orderModule = container.resolve(ModuleRegistrationName.ORDER) order = await orderModule.createOrders({ @@ -209,6 +222,7 @@ medusaIntegrationTestRunner({ { id: item.id, quantity: 2, + reason_id: returnReason.id, }, ], }, @@ -293,6 +307,7 @@ medusaIntegrationTestRunner({ { quantity: 2, internal_note: "Test internal note", + reason_id: returnReason.id, }, adminHeaders ) @@ -441,6 +456,21 @@ medusaIntegrationTestRunner({ adminHeaders ) + expect(result.data.return).toEqual( + expect.objectContaining({ + items: [ + expect.objectContaining({ + reason: expect.objectContaining({ + id: returnReason.id, + value: "return-reason-test", + label: "Test return reason", + description: "This is the reason description!!!", + }), + }), + ], + }) + ) + expect(result.data.order_preview).toEqual( expect.objectContaining({ id: order.id, diff --git a/packages/core/core-flows/src/order/steps/create-return-items.ts b/packages/core/core-flows/src/order/steps/create-return-items.ts index 3932da546e..c55f8e608a 100644 --- a/packages/core/core-flows/src/order/steps/create-return-items.ts +++ b/packages/core/core-flows/src/order/steps/create-return-items.ts @@ -1,6 +1,10 @@ -import { IOrderModuleService, OrderChangeActionDTO } from "@medusajs/types" +import { + IOrderModuleService, + OrderChangeActionDTO, + UpdateReturnDTO, +} from "@medusajs/types" import { ModuleRegistrationName } from "@medusajs/utils" -import { createStep, StepResponse } from "@medusajs/workflows-sdk" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" type CreateReturnItemsInput = { changes: OrderChangeActionDTO[] @@ -18,6 +22,7 @@ export const createReturnItems = createStep( return { return_id: item.reference_id, item_id: item.details?.reference_id, + reason_id: item.details?.reason_id, quantity: item.details?.quantity as number, note: item.internal_note, metadata: (item.details?.metadata as Record) ?? {}, @@ -33,7 +38,10 @@ export const createReturnItems = createStep( ) const createdReturnItems = await orderModuleService.updateReturns([ - { selector: { id: input.returnId }, data: { items: returnItems } }, + { + selector: { id: input.returnId }, + data: { items: returnItems as UpdateReturnDTO["items"] }, + }, ]) return new StepResponse(createdReturnItems, prevReturn) diff --git a/packages/core/core-flows/src/order/utils/validate-return-reason.ts b/packages/core/core-flows/src/order/utils/validate-return-reason.ts new file mode 100644 index 0000000000..84f5dfc02d --- /dev/null +++ b/packages/core/core-flows/src/order/utils/validate-return-reason.ts @@ -0,0 +1,64 @@ +import { + ContainerRegistrationKeys, + MedusaError, + arrayDifference, + remoteQueryObjectFromString, +} from "@medusajs/utils" + +export async function validateReturnReasons( + { + orderId, + inputItems, + }: { + orderId: string + inputItems: (unknown & { reason_id?: string | null })[] + }, + { container } +) { + const reasonIds = inputItems.map((i) => i.reason_id).filter(Boolean) + if (!reasonIds.length) { + return + } + + const remoteQuery = container.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const remoteQueryObject = remoteQueryObjectFromString({ + entryPoint: "return_reasons", + fields: [ + "id", + "parent_return_reason_id", + "parent_return_reason", + "return_reason_children.id", + ], + variables: { id: [inputItems.map((item) => item.reason_id)], limit: null }, + }) + + const returnReasons = await remoteQuery(remoteQueryObject) + + const reasons = returnReasons.map((r) => r.id) + const hasInvalidReasons = returnReasons + .filter( + // We do not allow for root reason to be applied + (reason) => reason.return_reason_children.length > 0 + ) + .map((r) => r.id) + const hasNonExistingReasons = arrayDifference(reasonIds, reasons) + + if (hasNonExistingReasons.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Return reason with id ${hasNonExistingReasons.join( + ", " + )} does not exists.` + ) + } + + if (hasInvalidReasons.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Cannot apply return reason with id ${hasInvalidReasons.join( + ", " + )} to order with id ${orderId}. Return reason has nested reasons.` + ) + } +} diff --git a/packages/core/core-flows/src/order/workflows/create-complete-return.ts b/packages/core/core-flows/src/order/workflows/create-complete-return.ts index a421fbe5d0..9e07ff2bf3 100644 --- a/packages/core/core-flows/src/order/workflows/create-complete-return.ts +++ b/packages/core/core-flows/src/order/workflows/create-complete-return.ts @@ -7,15 +7,7 @@ import { ShippingOptionDTO, WithCalculatedPrice, } from "@medusajs/types" -import { - ContainerRegistrationKeys, - MathBN, - MedusaError, - Modules, - arrayDifference, - isDefined, - remoteQueryObjectFromString, -} from "@medusajs/utils" +import { MathBN, MedusaError, Modules, isDefined } from "@medusajs/utils" import { WorkflowData, createStep, @@ -31,64 +23,7 @@ import { throwIfItemsDoesNotExistsInOrder, throwIfOrderIsCancelled, } from "../utils/order-validation" - -async function validateReturnReasons( - { - orderId, - inputItems, - }: { - orderId: string - inputItems: OrderWorkflow.CreateOrderReturnWorkflowInput["items"] - }, - { container } -) { - const reasonIds = inputItems.map((i) => i.reason_id).filter(Boolean) - if (!reasonIds.length) { - return - } - - const remoteQuery = container.resolve(ContainerRegistrationKeys.REMOTE_QUERY) - - const remoteQueryObject = remoteQueryObjectFromString({ - entryPoint: "return_reasons", - fields: [ - "id", - "parent_return_reason_id", - "parent_return_reason", - "return_reason_children.id", - ], - variables: { id: [inputItems.map((item) => item.reason_id)], limit: null }, - }) - - const returnReasons = await remoteQuery(remoteQueryObject) - - const reasons = returnReasons.map((r) => r.id) - const hasInvalidReasons = returnReasons - .filter( - // We do not allow for root reason to be applied - (reason) => reason.return_reason_children.length > 0 - ) - .map((r) => r.id) - const hasNonExistingReasons = arrayDifference(reasonIds, reasons) - - if (hasNonExistingReasons.length) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Return reason with id ${hasNonExistingReasons.join( - ", " - )} does not exists.` - ) - } - - if (hasInvalidReasons.length) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Cannot apply return reason with id ${hasInvalidReasons.join( - ", " - )} to order with id ${orderId}. Return reason has nested reasons.` - ) - } -} +import { validateReturnReasons } from "../utils/validate-return-reason" function prepareShippingMethodData({ orderId, diff --git a/packages/core/core-flows/src/order/workflows/return/request-item-return.ts b/packages/core/core-flows/src/order/workflows/return/request-item-return.ts index 072cd373fd..43c0141fd3 100644 --- a/packages/core/core-flows/src/order/workflows/return/request-item-return.ts +++ b/packages/core/core-flows/src/order/workflows/return/request-item-return.ts @@ -19,24 +19,33 @@ import { throwIfItemsDoesNotExistsInOrder, throwIfOrderChangeIsNotActive, } from "../../utils/order-validation" +import { validateReturnReasons } from "../../utils/validate-return-reason" const validationStep = createStep( "request-item-return-validation", - async function ({ - order, - orderChange, - orderReturn, - items, - }: { - order: Pick - orderReturn: ReturnDTO - orderChange: OrderChangeDTO - items: OrderWorkflow.RequestItemReturnWorkflowInput["items"] - }) { + async function ( + { + order, + orderChange, + orderReturn, + items, + }: { + order: Pick + orderReturn: ReturnDTO + orderChange: OrderChangeDTO + items: OrderWorkflow.RequestItemReturnWorkflowInput["items"] + }, + context + ) { throwIfIsCancelled(order, "Order") throwIfIsCancelled(orderReturn, "Return") throwIfOrderChangeIsNotActive({ orderChange }) throwIfItemsDoesNotExistsInOrder({ order, inputItems: items }) + + await validateReturnReasons( + { orderId: order.id, inputItems: items }, + context + ) } ) @@ -87,6 +96,7 @@ export const requestItemReturnWorkflow = createWorkflow( reference_id: orderReturn.id, details: { reference_id: item.id, + reason_id: item.reason_id, quantity: item.quantity, metadata: item.metadata, }, diff --git a/packages/core/core-flows/src/order/workflows/return/update-request-item-return.ts b/packages/core/core-flows/src/order/workflows/return/update-request-item-return.ts index 2db84323bf..6b5d336616 100644 --- a/packages/core/core-flows/src/order/workflows/return/update-request-item-return.ts +++ b/packages/core/core-flows/src/order/workflows/return/update-request-item-return.ts @@ -21,20 +21,24 @@ import { throwIfIsCancelled, throwIfOrderChangeIsNotActive, } from "../../utils/order-validation" +import { validateReturnReasons } from "../../utils/validate-return-reason" const validationStep = createStep( "update-request-item-return-validation", - async function ({ - order, - orderChange, - orderReturn, - input, - }: { - order: OrderDTO - orderReturn: ReturnDTO - orderChange: OrderChangeDTO - input: OrderWorkflow.UpdateRequestItemReturnWorkflowInput - }) { + async function ( + { + order, + orderChange, + orderReturn, + input, + }: { + order: OrderDTO + orderReturn: ReturnDTO + orderChange: OrderChangeDTO + input: OrderWorkflow.UpdateRequestItemReturnWorkflowInput + }, + context + ) { throwIfIsCancelled(order, "Order") throwIfIsCancelled(orderReturn, "Return") throwIfOrderChangeIsNotActive({ orderChange }) @@ -52,6 +56,16 @@ const validationStep = createStep( `Action ${associatedAction.id} is not requesting item return` ) } + + if (input.data.reason_id) { + await validateReturnReasons( + { + orderId: order.id, + inputItems: [{ reason_id: input.data.reason_id }], + }, + context + ) + } } ) @@ -104,6 +118,8 @@ export const updateRequestItemReturnWorkflow = createWorkflow( id: input.action_id, details: { quantity: data.quantity ?? originalAction.details?.quantity, + reason_id: data.reason_id ?? originalAction.details?.reason_id, + metadata: data.metadata ?? originalAction.details?.metadata, }, internal_note: data.internal_note, } diff --git a/packages/core/types/src/order/mutations.ts b/packages/core/types/src/order/mutations.ts index 02c8302104..6122fcff78 100644 --- a/packages/core/types/src/order/mutations.ts +++ b/packages/core/types/src/order/mutations.ts @@ -445,7 +445,6 @@ export interface UpdateReturnDTO { metadata?: Record | null items?: { quantity: BigNumberInput - internal_note?: string | null note?: string | null reason_id?: string | null metadata?: Record | null 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 18ddb25bb0..41fc98821d 100644 --- a/packages/core/types/src/workflow/order/create-return-order.ts +++ b/packages/core/types/src/workflow/order/create-return-order.ts @@ -6,7 +6,7 @@ export interface CreateReturnItem { internal_note?: string | null reason_id?: string | null note?: string | null - metadata?: Record + metadata?: Record | null } export interface CreateOrderReturnWorkflowInput { diff --git a/packages/core/types/src/workflow/order/request-item-return.ts b/packages/core/types/src/workflow/order/request-item-return.ts index 8aec8b4b75..67e29ff061 100644 --- a/packages/core/types/src/workflow/order/request-item-return.ts +++ b/packages/core/types/src/workflow/order/request-item-return.ts @@ -16,6 +16,8 @@ export interface UpdateRequestItemReturnWorkflowInput { data: { quantity?: BigNumberInput internal_note?: string | null + reason_id?: string | null + metadata?: Record | null } } diff --git a/packages/medusa/src/api/admin/return-reasons/query-config.ts b/packages/medusa/src/api/admin/return-reasons/query-config.ts index e1ca20a982..e43675e7f8 100644 --- a/packages/medusa/src/api/admin/return-reasons/query-config.ts +++ b/packages/medusa/src/api/admin/return-reasons/query-config.ts @@ -1,12 +1,12 @@ export const defaultAdminReturnReasonFields = [ "id", - "display_id", - "status", - "version", - "summary", - "metadata", + "value", + "label", + "parent_return_reason_id", + "description", "created_at", "updated_at", + "deleted_at", ] export const defaultAdminRetrieveReturnReasonFields = [ @@ -18,8 +18,8 @@ export const defaultAdminRetrieveReturnReasonFields = [ "created_at", "updated_at", "deleted_at", - "*.parent_return_reason", - "*.return_reason_children", + "parent_return_reason.*", + "return_reason_children.*", ] export const retrieveTransformQueryConfig = { diff --git a/packages/medusa/src/api/admin/return-reasons/validators.ts b/packages/medusa/src/api/admin/return-reasons/validators.ts index 861c7fa97d..1fc5130e75 100644 --- a/packages/medusa/src/api/admin/return-reasons/validators.ts +++ b/packages/medusa/src/api/admin/return-reasons/validators.ts @@ -52,7 +52,7 @@ export type AdminGetReturnReasonsParamsType = z.infer< export const AdminCreateReturnReason = z.object({ value: z.string(), label: z.string(), - descripton: z.string().nullish(), + description: z.string().nullish(), parent_return_reason_id: z.string().nullish(), metadata: z.record(z.unknown()).nullish(), }) @@ -63,7 +63,7 @@ export type AdminCreateReturnReasonType = z.infer< export const AdminUpdateReturnReason = z.object({ value: z.string(), label: z.string(), - descripton: z.string().nullish(), + description: z.string().nullish(), metadata: z.record(z.unknown()).nullish(), }) export type AdminUpdateReturnReasonType = z.infer< diff --git a/packages/medusa/src/api/admin/returns/query-config.ts b/packages/medusa/src/api/admin/returns/query-config.ts index e3b8e510af..d4e368775c 100644 --- a/packages/medusa/src/api/admin/returns/query-config.ts +++ b/packages/medusa/src/api/admin/returns/query-config.ts @@ -11,8 +11,14 @@ export const defaultAdminReturnFields = [ "updated_at", ] +export const defaultAdminDetailsReturnFields = [ + ...defaultAdminReturnFields, + "items.*", + "items.reason.*", +] + export const retrieveTransformQueryConfig = { - defaultFields: defaultAdminReturnFields, + defaultFields: defaultAdminDetailsReturnFields, isList: false, } diff --git a/packages/medusa/src/api/admin/returns/validators.ts b/packages/medusa/src/api/admin/returns/validators.ts index 69473b8d44..edd858952f 100644 --- a/packages/medusa/src/api/admin/returns/validators.ts +++ b/packages/medusa/src/api/admin/returns/validators.ts @@ -125,6 +125,7 @@ export const AdminPostReturnsRequestItemsReqSchema = z.object({ quantity: z.number(), description: z.string().optional(), internal_note: z.string().optional(), + reason_id: z.string().optional(), metadata: z.record(z.unknown()).optional(), }) ), @@ -137,6 +138,8 @@ export type AdminPostReturnsRequestItemsReqSchemaType = z.infer< export const AdminPostReturnsRequestItemsActionReqSchema = z.object({ quantity: z.number().optional(), internal_note: z.string().nullish().optional(), + reason_id: z.string().nullish().optional(), + metadata: z.record(z.unknown()).nullish().optional(), }) export type AdminPostReturnsRequestItemsActionReqSchemaType = z.infer< diff --git a/packages/modules/order/integration-tests/__tests__/returns.ts b/packages/modules/order/integration-tests/__tests__/returns.ts index 8f07b2c973..a17142dd98 100644 --- a/packages/modules/order/integration-tests/__tests__/returns.ts +++ b/packages/modules/order/integration-tests/__tests__/returns.ts @@ -1,6 +1,6 @@ import { IOrderModuleService } from "@medusajs/types" -import { moduleIntegrationTestRunner } from "medusa-test-utils" import { Modules } from "@medusajs/utils" +import { moduleIntegrationTestRunner } from "medusa-test-utils" jest.setTimeout(100000) @@ -15,15 +15,17 @@ moduleIntegrationTestRunner({ description: "description test", }) - expect(reason).toEqual({ - id: expect.any(String), - value: "test", - label: "label test", - description: "description test", - return_reason_children: [], - metadata: null, - deleted_at: null, - }) + expect(reason).toEqual( + expect.objectContaining({ + id: expect.any(String), + value: "test", + label: "label test", + description: "description test", + return_reason_children: [], + metadata: null, + deleted_at: null, + }) + ) }) it("should create return reasons with parent", async function () { diff --git a/packages/modules/order/src/services/order-module-service.ts b/packages/modules/order/src/services/order-module-service.ts index 3814936539..27321826b0 100644 --- a/packages/modules/order/src/services/order-module-service.ts +++ b/packages/modules/order/src/services/order-module-service.ts @@ -11,7 +11,6 @@ import { RestoreReturn, SoftDeleteReturn, UpdateOrderItemWithSelectorDTO, - UpdateOrderReturnReasonDTO, } from "@medusajs/types" import { BigNumber, @@ -2910,146 +2909,6 @@ export default class OrderModuleService< await this.orderSummaryService_.update(summaries, sharedContext) } - // @ts-ignore - async createReturnReasons( - transactionData: OrderTypes.CreateOrderReturnReasonDTO, - sharedContext?: Context - ): Promise - - async createReturnReasons( - transactionData: OrderTypes.CreateOrderReturnReasonDTO[], - sharedContext?: Context - ): Promise - - @InjectTransactionManager("baseRepository_") - async createReturnReasons( - returnReasonData: - | OrderTypes.CreateOrderReturnReasonDTO - | OrderTypes.CreateOrderReturnReasonDTO[], - @MedusaContext() sharedContext?: Context - ): Promise< - OrderTypes.OrderReturnReasonDTO | OrderTypes.OrderReturnReasonDTO[] - > { - const data = Array.isArray(returnReasonData) - ? returnReasonData - : [returnReasonData] - - const created = await this.returnReasonService_.create(data, sharedContext) - - return await this.baseRepository_.serialize( - !Array.isArray(returnReasonData) ? created[0] : created, - { - populate: true, - } - ) - } - - // @ts-ignore - updateReturnReasons( - data: OrderTypes.UpdateOrderReturnReasonWithSelectorDTO[] - ): Promise - - updateReturnReasons( - selector: Partial, - data: OrderTypes.UpdateOrderReturnReasonDTO, - sharedContext?: Context - ): Promise - - updateReturnReasons( - id: string, - data: Partial, - sharedContext?: Context - ): Promise - - @InjectManager("baseRepository_") - async updateReturnReasons( - idOrDataOrSelector: - | string - | OrderTypes.UpdateOrderReturnReasonWithSelectorDTO[] - | Partial, - data?: - | OrderTypes.UpdateOrderReturnReasonDTO - | Partial, - @MedusaContext() sharedContext: Context = {} - ): Promise< - OrderTypes.OrderReturnReasonDTO[] | OrderTypes.OrderReturnReasonDTO - > { - let reasons: ReturnReason[] = [] - if (isString(idOrDataOrSelector)) { - const reason = await this.updateReturnReason_( - idOrDataOrSelector, - data as Partial, - sharedContext - ) - - return await this.baseRepository_.serialize( - reason, - { - populate: true, - } - ) - } - - const toUpdate = Array.isArray(idOrDataOrSelector) - ? idOrDataOrSelector - : [ - { - selector: idOrDataOrSelector, - data: data, - } as OrderTypes.UpdateOrderReturnReasonWithSelectorDTO, - ] - - reasons = await this.updateReturnReasonsWithSelector_( - toUpdate, - sharedContext - ) - - return await this.baseRepository_.serialize< - OrderTypes.OrderReturnReasonDTO[] - >(reasons, { - populate: true, - }) - } - - @InjectTransactionManager("baseRepository_") - protected async updateReturnReason_( - reasonId: string, - data: Partial, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const [reason] = await this.returnReasonService_.update( - [{ id: reasonId, ...data }], - sharedContext - ) - - return reason - } - - @InjectTransactionManager("baseRepository_") - protected async updateReturnReasonsWithSelector_( - updates: OrderTypes.UpdateOrderReturnReasonWithSelectorDTO[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - let toUpdate: UpdateOrderReturnReasonDTO[] = [] - - for (const { selector, data } of updates) { - const reasons = await super.listReturnReasons( - { ...selector }, - {}, - sharedContext - ) - - reasons.forEach((reason) => { - toUpdate.push({ - ...data, - id: reason.id, - }) - }) - } - - return await this.returnReasonService_.update(toUpdate, sharedContext) - } - async archive( orderId: string, sharedContext?: Context