fix(core-flow): request item return reason (#8152)

This commit is contained in:
Carlos R. L. Rodrigues
2024-07-17 05:35:33 -03:00
committed by GitHub
parent 1d40b3cc98
commit d4fe2daa57
15 changed files with 189 additions and 255 deletions
@@ -13,11 +13,24 @@ medusaIntegrationTestRunner({
let returnShippingOption let returnShippingOption
let shippingProfile let shippingProfile
let fulfillmentSet let fulfillmentSet
let returnReason
beforeEach(async () => { beforeEach(async () => {
const container = getContainer() const container = getContainer()
await createAdminUser(dbConnection, adminHeaders, container) 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) const orderModule = container.resolve(ModuleRegistrationName.ORDER)
order = await orderModule.createOrders({ order = await orderModule.createOrders({
@@ -209,6 +222,7 @@ medusaIntegrationTestRunner({
{ {
id: item.id, id: item.id,
quantity: 2, quantity: 2,
reason_id: returnReason.id,
}, },
], ],
}, },
@@ -293,6 +307,7 @@ medusaIntegrationTestRunner({
{ {
quantity: 2, quantity: 2,
internal_note: "Test internal note", internal_note: "Test internal note",
reason_id: returnReason.id,
}, },
adminHeaders adminHeaders
) )
@@ -441,6 +456,21 @@ medusaIntegrationTestRunner({
adminHeaders 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(result.data.order_preview).toEqual(
expect.objectContaining({ expect.objectContaining({
id: order.id, id: order.id,
@@ -1,6 +1,10 @@
import { IOrderModuleService, OrderChangeActionDTO } from "@medusajs/types" import {
IOrderModuleService,
OrderChangeActionDTO,
UpdateReturnDTO,
} from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/utils" import { ModuleRegistrationName } from "@medusajs/utils"
import { createStep, StepResponse } from "@medusajs/workflows-sdk" import { StepResponse, createStep } from "@medusajs/workflows-sdk"
type CreateReturnItemsInput = { type CreateReturnItemsInput = {
changes: OrderChangeActionDTO[] changes: OrderChangeActionDTO[]
@@ -18,6 +22,7 @@ export const createReturnItems = createStep(
return { return {
return_id: item.reference_id, return_id: item.reference_id,
item_id: item.details?.reference_id, item_id: item.details?.reference_id,
reason_id: item.details?.reason_id,
quantity: item.details?.quantity as number, quantity: item.details?.quantity as number,
note: item.internal_note, note: item.internal_note,
metadata: (item.details?.metadata as Record<string, unknown>) ?? {}, metadata: (item.details?.metadata as Record<string, unknown>) ?? {},
@@ -33,7 +38,10 @@ export const createReturnItems = createStep(
) )
const createdReturnItems = await orderModuleService.updateReturns([ 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) return new StepResponse(createdReturnItems, prevReturn)
@@ -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.`
)
}
}
@@ -7,15 +7,7 @@ import {
ShippingOptionDTO, ShippingOptionDTO,
WithCalculatedPrice, WithCalculatedPrice,
} from "@medusajs/types" } from "@medusajs/types"
import { import { MathBN, MedusaError, Modules, isDefined } from "@medusajs/utils"
ContainerRegistrationKeys,
MathBN,
MedusaError,
Modules,
arrayDifference,
isDefined,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { import {
WorkflowData, WorkflowData,
createStep, createStep,
@@ -31,64 +23,7 @@ import {
throwIfItemsDoesNotExistsInOrder, throwIfItemsDoesNotExistsInOrder,
throwIfOrderIsCancelled, throwIfOrderIsCancelled,
} from "../utils/order-validation" } from "../utils/order-validation"
import { validateReturnReasons } from "../utils/validate-return-reason"
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.`
)
}
}
function prepareShippingMethodData({ function prepareShippingMethodData({
orderId, orderId,
@@ -19,24 +19,33 @@ import {
throwIfItemsDoesNotExistsInOrder, throwIfItemsDoesNotExistsInOrder,
throwIfOrderChangeIsNotActive, throwIfOrderChangeIsNotActive,
} from "../../utils/order-validation" } from "../../utils/order-validation"
import { validateReturnReasons } from "../../utils/validate-return-reason"
const validationStep = createStep( const validationStep = createStep(
"request-item-return-validation", "request-item-return-validation",
async function ({ async function (
order, {
orderChange, order,
orderReturn, orderChange,
items, orderReturn,
}: { items,
order: Pick<OrderDTO, "id" | "items"> }: {
orderReturn: ReturnDTO order: Pick<OrderDTO, "id" | "items">
orderChange: OrderChangeDTO orderReturn: ReturnDTO
items: OrderWorkflow.RequestItemReturnWorkflowInput["items"] orderChange: OrderChangeDTO
}) { items: OrderWorkflow.RequestItemReturnWorkflowInput["items"]
},
context
) {
throwIfIsCancelled(order, "Order") throwIfIsCancelled(order, "Order")
throwIfIsCancelled(orderReturn, "Return") throwIfIsCancelled(orderReturn, "Return")
throwIfOrderChangeIsNotActive({ orderChange }) throwIfOrderChangeIsNotActive({ orderChange })
throwIfItemsDoesNotExistsInOrder({ order, inputItems: items }) throwIfItemsDoesNotExistsInOrder({ order, inputItems: items })
await validateReturnReasons(
{ orderId: order.id, inputItems: items },
context
)
} }
) )
@@ -87,6 +96,7 @@ export const requestItemReturnWorkflow = createWorkflow(
reference_id: orderReturn.id, reference_id: orderReturn.id,
details: { details: {
reference_id: item.id, reference_id: item.id,
reason_id: item.reason_id,
quantity: item.quantity, quantity: item.quantity,
metadata: item.metadata, metadata: item.metadata,
}, },
@@ -21,20 +21,24 @@ import {
throwIfIsCancelled, throwIfIsCancelled,
throwIfOrderChangeIsNotActive, throwIfOrderChangeIsNotActive,
} from "../../utils/order-validation" } from "../../utils/order-validation"
import { validateReturnReasons } from "../../utils/validate-return-reason"
const validationStep = createStep( const validationStep = createStep(
"update-request-item-return-validation", "update-request-item-return-validation",
async function ({ async function (
order, {
orderChange, order,
orderReturn, orderChange,
input, orderReturn,
}: { input,
order: OrderDTO }: {
orderReturn: ReturnDTO order: OrderDTO
orderChange: OrderChangeDTO orderReturn: ReturnDTO
input: OrderWorkflow.UpdateRequestItemReturnWorkflowInput orderChange: OrderChangeDTO
}) { input: OrderWorkflow.UpdateRequestItemReturnWorkflowInput
},
context
) {
throwIfIsCancelled(order, "Order") throwIfIsCancelled(order, "Order")
throwIfIsCancelled(orderReturn, "Return") throwIfIsCancelled(orderReturn, "Return")
throwIfOrderChangeIsNotActive({ orderChange }) throwIfOrderChangeIsNotActive({ orderChange })
@@ -52,6 +56,16 @@ const validationStep = createStep(
`Action ${associatedAction.id} is not requesting item return` `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, id: input.action_id,
details: { details: {
quantity: data.quantity ?? originalAction.details?.quantity, 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, internal_note: data.internal_note,
} }
@@ -445,7 +445,6 @@ export interface UpdateReturnDTO {
metadata?: Record<string, unknown> | null metadata?: Record<string, unknown> | null
items?: { items?: {
quantity: BigNumberInput quantity: BigNumberInput
internal_note?: string | null
note?: string | null note?: string | null
reason_id?: string | null reason_id?: string | null
metadata?: Record<string, unknown> | null metadata?: Record<string, unknown> | null
@@ -6,7 +6,7 @@ export interface CreateReturnItem {
internal_note?: string | null internal_note?: string | null
reason_id?: string | null reason_id?: string | null
note?: string | null note?: string | null
metadata?: Record<string, any> metadata?: Record<string, any> | null
} }
export interface CreateOrderReturnWorkflowInput { export interface CreateOrderReturnWorkflowInput {
@@ -16,6 +16,8 @@ export interface UpdateRequestItemReturnWorkflowInput {
data: { data: {
quantity?: BigNumberInput quantity?: BigNumberInput
internal_note?: string | null internal_note?: string | null
reason_id?: string | null
metadata?: Record<string, any> | null
} }
} }
@@ -1,12 +1,12 @@
export const defaultAdminReturnReasonFields = [ export const defaultAdminReturnReasonFields = [
"id", "id",
"display_id", "value",
"status", "label",
"version", "parent_return_reason_id",
"summary", "description",
"metadata",
"created_at", "created_at",
"updated_at", "updated_at",
"deleted_at",
] ]
export const defaultAdminRetrieveReturnReasonFields = [ export const defaultAdminRetrieveReturnReasonFields = [
@@ -18,8 +18,8 @@ export const defaultAdminRetrieveReturnReasonFields = [
"created_at", "created_at",
"updated_at", "updated_at",
"deleted_at", "deleted_at",
"*.parent_return_reason", "parent_return_reason.*",
"*.return_reason_children", "return_reason_children.*",
] ]
export const retrieveTransformQueryConfig = { export const retrieveTransformQueryConfig = {
@@ -52,7 +52,7 @@ export type AdminGetReturnReasonsParamsType = z.infer<
export const AdminCreateReturnReason = z.object({ export const AdminCreateReturnReason = z.object({
value: z.string(), value: z.string(),
label: z.string(), label: z.string(),
descripton: z.string().nullish(), description: z.string().nullish(),
parent_return_reason_id: z.string().nullish(), parent_return_reason_id: z.string().nullish(),
metadata: z.record(z.unknown()).nullish(), metadata: z.record(z.unknown()).nullish(),
}) })
@@ -63,7 +63,7 @@ export type AdminCreateReturnReasonType = z.infer<
export const AdminUpdateReturnReason = z.object({ export const AdminUpdateReturnReason = z.object({
value: z.string(), value: z.string(),
label: z.string(), label: z.string(),
descripton: z.string().nullish(), description: z.string().nullish(),
metadata: z.record(z.unknown()).nullish(), metadata: z.record(z.unknown()).nullish(),
}) })
export type AdminUpdateReturnReasonType = z.infer< export type AdminUpdateReturnReasonType = z.infer<
@@ -11,8 +11,14 @@ export const defaultAdminReturnFields = [
"updated_at", "updated_at",
] ]
export const defaultAdminDetailsReturnFields = [
...defaultAdminReturnFields,
"items.*",
"items.reason.*",
]
export const retrieveTransformQueryConfig = { export const retrieveTransformQueryConfig = {
defaultFields: defaultAdminReturnFields, defaultFields: defaultAdminDetailsReturnFields,
isList: false, isList: false,
} }
@@ -125,6 +125,7 @@ export const AdminPostReturnsRequestItemsReqSchema = z.object({
quantity: z.number(), quantity: z.number(),
description: z.string().optional(), description: z.string().optional(),
internal_note: z.string().optional(), internal_note: z.string().optional(),
reason_id: z.string().optional(),
metadata: z.record(z.unknown()).optional(), metadata: z.record(z.unknown()).optional(),
}) })
), ),
@@ -137,6 +138,8 @@ export type AdminPostReturnsRequestItemsReqSchemaType = z.infer<
export const AdminPostReturnsRequestItemsActionReqSchema = z.object({ export const AdminPostReturnsRequestItemsActionReqSchema = z.object({
quantity: z.number().optional(), quantity: z.number().optional(),
internal_note: z.string().nullish().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< export type AdminPostReturnsRequestItemsActionReqSchemaType = z.infer<
@@ -1,6 +1,6 @@
import { IOrderModuleService } from "@medusajs/types" import { IOrderModuleService } from "@medusajs/types"
import { moduleIntegrationTestRunner } from "medusa-test-utils"
import { Modules } from "@medusajs/utils" import { Modules } from "@medusajs/utils"
import { moduleIntegrationTestRunner } from "medusa-test-utils"
jest.setTimeout(100000) jest.setTimeout(100000)
@@ -15,15 +15,17 @@ moduleIntegrationTestRunner<IOrderModuleService>({
description: "description test", description: "description test",
}) })
expect(reason).toEqual({ expect(reason).toEqual(
id: expect.any(String), expect.objectContaining({
value: "test", id: expect.any(String),
label: "label test", value: "test",
description: "description test", label: "label test",
return_reason_children: [], description: "description test",
metadata: null, return_reason_children: [],
deleted_at: null, metadata: null,
}) deleted_at: null,
})
)
}) })
it("should create return reasons with parent", async function () { it("should create return reasons with parent", async function () {
@@ -11,7 +11,6 @@ import {
RestoreReturn, RestoreReturn,
SoftDeleteReturn, SoftDeleteReturn,
UpdateOrderItemWithSelectorDTO, UpdateOrderItemWithSelectorDTO,
UpdateOrderReturnReasonDTO,
} from "@medusajs/types" } from "@medusajs/types"
import { import {
BigNumber, BigNumber,
@@ -2910,146 +2909,6 @@ export default class OrderModuleService<
await this.orderSummaryService_.update(summaries, sharedContext) await this.orderSummaryService_.update(summaries, sharedContext)
} }
// @ts-ignore
async createReturnReasons(
transactionData: OrderTypes.CreateOrderReturnReasonDTO,
sharedContext?: Context
): Promise<OrderTypes.OrderReturnReasonDTO>
async createReturnReasons(
transactionData: OrderTypes.CreateOrderReturnReasonDTO[],
sharedContext?: Context
): Promise<OrderTypes.OrderReturnReasonDTO[]>
@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<OrderTypes.OrderReturnReasonDTO>(
!Array.isArray(returnReasonData) ? created[0] : created,
{
populate: true,
}
)
}
// @ts-ignore
updateReturnReasons(
data: OrderTypes.UpdateOrderReturnReasonWithSelectorDTO[]
): Promise<OrderTypes.OrderReturnReasonDTO[]>
updateReturnReasons(
selector: Partial<OrderTypes.FilterableOrderReturnReasonProps>,
data: OrderTypes.UpdateOrderReturnReasonDTO,
sharedContext?: Context
): Promise<OrderTypes.OrderReturnReasonDTO[]>
updateReturnReasons(
id: string,
data: Partial<OrderTypes.UpdateOrderReturnReasonDTO>,
sharedContext?: Context
): Promise<OrderTypes.OrderReturnReasonDTO>
@InjectManager("baseRepository_")
async updateReturnReasons(
idOrDataOrSelector:
| string
| OrderTypes.UpdateOrderReturnReasonWithSelectorDTO[]
| Partial<OrderTypes.FilterableOrderReturnReasonProps>,
data?:
| OrderTypes.UpdateOrderReturnReasonDTO
| Partial<OrderTypes.UpdateOrderReturnReasonDTO>,
@MedusaContext() sharedContext: Context = {}
): Promise<
OrderTypes.OrderReturnReasonDTO[] | OrderTypes.OrderReturnReasonDTO
> {
let reasons: ReturnReason[] = []
if (isString(idOrDataOrSelector)) {
const reason = await this.updateReturnReason_(
idOrDataOrSelector,
data as Partial<OrderTypes.UpdateOrderReturnReasonDTO>,
sharedContext
)
return await this.baseRepository_.serialize<OrderTypes.OrderReturnReasonDTO>(
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<OrderTypes.UpdateOrderReturnReasonDTO>,
@MedusaContext() sharedContext: Context = {}
): Promise<ReturnReason> {
const [reason] = await this.returnReasonService_.update(
[{ id: reasonId, ...data }],
sharedContext
)
return reason
}
@InjectTransactionManager("baseRepository_")
protected async updateReturnReasonsWithSelector_(
updates: OrderTypes.UpdateOrderReturnReasonWithSelectorDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<ReturnReason[]> {
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( async archive(
orderId: string, orderId: string,
sharedContext?: Context sharedContext?: Context