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

View File

@@ -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,

View File

@@ -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<string, unknown>) ?? {},
@@ -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)

View File

@@ -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.`
)
}
}

View File

@@ -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,

View File

@@ -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<OrderDTO, "id" | "items">
orderReturn: ReturnDTO
orderChange: OrderChangeDTO
items: OrderWorkflow.RequestItemReturnWorkflowInput["items"]
}) {
async function (
{
order,
orderChange,
orderReturn,
items,
}: {
order: Pick<OrderDTO, "id" | "items">
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,
},

View File

@@ -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,
}

View File

@@ -445,7 +445,6 @@ export interface UpdateReturnDTO {
metadata?: Record<string, unknown> | null
items?: {
quantity: BigNumberInput
internal_note?: string | null
note?: string | null
reason_id?: string | null
metadata?: Record<string, unknown> | null

View File

@@ -6,7 +6,7 @@ export interface CreateReturnItem {
internal_note?: string | null
reason_id?: string | null
note?: string | null
metadata?: Record<string, any>
metadata?: Record<string, any> | null
}
export interface CreateOrderReturnWorkflowInput {

View File

@@ -16,6 +16,8 @@ export interface UpdateRequestItemReturnWorkflowInput {
data: {
quantity?: BigNumberInput
internal_note?: string | null
reason_id?: string | null
metadata?: Record<string, any> | null
}
}

View File

@@ -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 = {

View File

@@ -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<

View File

@@ -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,
}

View File

@@ -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<

View File

@@ -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<IOrderModuleService>({
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 () {

View File

@@ -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<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(
orderId: string,
sharedContext?: Context