diff --git a/packages/core/core-flows/src/order/steps/cancel-claim.ts b/packages/core/core-flows/src/order/steps/cancel-claim.ts new file mode 100644 index 0000000000..2f6b354983 --- /dev/null +++ b/packages/core/core-flows/src/order/steps/cancel-claim.ts @@ -0,0 +1,29 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { CancelOrderClaimDTO, IOrderModuleService } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +type CancelOrderClaimStepInput = CancelOrderClaimDTO + +export const cancelOrderClaimStepId = "cancel-order-claim" +export const cancelOrderClaimStep = createStep( + cancelOrderClaimStepId, + async (data: CancelOrderClaimStepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + await service.cancelClaim(data) + return new StepResponse(void 0, data.return_id) + }, + async (orderId, { container }) => { + if (!orderId) { + return + } + + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + await service.revertLastVersion(orderId) + } +) diff --git a/packages/core/core-flows/src/order/steps/cancel-exchange.ts b/packages/core/core-flows/src/order/steps/cancel-exchange.ts new file mode 100644 index 0000000000..b1cddcdd9a --- /dev/null +++ b/packages/core/core-flows/src/order/steps/cancel-exchange.ts @@ -0,0 +1,29 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { CancelOrderExchangeDTO, IOrderModuleService } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +type CancelOrderExchangeStepInput = CancelOrderExchangeDTO + +export const cancelOrderExchangeStepId = "cancel-order-swap" +export const cancelOrderExchangeStep = createStep( + cancelOrderExchangeStepId, + async (data: CancelOrderExchangeStepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + await service.cancelExchange(data) + return new StepResponse(void 0, data.return_id) + }, + async (orderId, { container }) => { + if (!orderId) { + return + } + + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + await service.revertLastVersion(orderId) + } +) diff --git a/packages/core/core-flows/src/order/steps/cancel-return.ts b/packages/core/core-flows/src/order/steps/cancel-return.ts new file mode 100644 index 0000000000..099c9bede2 --- /dev/null +++ b/packages/core/core-flows/src/order/steps/cancel-return.ts @@ -0,0 +1,29 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { CancelOrderReturnDTO, IOrderModuleService } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +type CancelOrderReturnStepInput = CancelOrderReturnDTO + +export const cancelOrderReturnStepId = "cancel-order-return" +export const cancelOrderReturnStep = createStep( + cancelOrderReturnStepId, + async (data: CancelOrderReturnStepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + await service.cancelReturn(data) + return new StepResponse(void 0, data.return_id) + }, + async (orderId, { container }) => { + if (!orderId) { + return + } + + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + await service.revertLastVersion(orderId) + } +) diff --git a/packages/core/core-flows/src/order/steps/index.ts b/packages/core/core-flows/src/order/steps/index.ts index 35a039dbe5..2f64e2d4d6 100644 --- a/packages/core/core-flows/src/order/steps/index.ts +++ b/packages/core/core-flows/src/order/steps/index.ts @@ -1,5 +1,8 @@ export * from "./archive-orders" +export * from "./cancel-claim" +export * from "./cancel-exchange" export * from "./cancel-orders" +export * from "./cancel-return" export * from "./complete-orders" export * from "./create-orders" export * from "./get-item-tax-lines" diff --git a/packages/core/core-flows/src/order/workflows/cancel-order.ts b/packages/core/core-flows/src/order/workflows/cancel-order.ts index 6396877026..65c46ac011 100644 --- a/packages/core/core-flows/src/order/workflows/cancel-order.ts +++ b/packages/core/core-flows/src/order/workflows/cancel-order.ts @@ -60,7 +60,7 @@ const validateOrder = createStep( pred: (obj: any) => boolean, type: string ) => { - if (arr?.filter(pred).length) { + if (arr?.some(pred)) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, `All ${type} must be canceled before canceling an order` diff --git a/packages/core/core-flows/src/order/workflows/cancel-return.ts b/packages/core/core-flows/src/order/workflows/cancel-return.ts new file mode 100644 index 0000000000..332213642d --- /dev/null +++ b/packages/core/core-flows/src/order/workflows/cancel-return.ts @@ -0,0 +1,83 @@ +import { + FulfillmentDTO, + OrderWorkflow, + PaymentCollectionDTO, + ReturnDTO, +} from "@medusajs/types" +import { MathBN, MedusaError } from "@medusajs/utils" +import { + WorkflowData, + createStep, + createWorkflow, +} from "@medusajs/workflows-sdk" +import { useRemoteQueryStep } from "../../common" +import { cancelOrderReturnStep } from "../steps" +import { throwIfReturnIsCancelled } from "../utils/order-validation" + +const validateOrder = createStep( + "validate-return", + ({ + orderReturn, + }: { + orderReturn: ReturnDTO + input: OrderWorkflow.CancelReturnWorkflowInput + }) => { + const orderReturn_ = orderReturn as ReturnDTO & { + payment_collections: PaymentCollectionDTO[] + fulfillments: FulfillmentDTO[] + } + + throwIfReturnIsCancelled({ orderReturn }) + + const throwErrorIf = ( + arr: unknown[], + pred: (obj: any) => boolean, + message: string + ) => { + if (arr?.some(pred)) { + throw new MedusaError(MedusaError.Types.NOT_ALLOWED, message) + } + } + + const notCanceled = (o) => !o.canceled_at + const hasReceived = (o) => MathBN.gt(o.received_quantity, 0) + + throwErrorIf( + orderReturn_.fulfillments, + notCanceled, + "All fulfillments must be canceled before canceling a return" + ) + + throwErrorIf( + orderReturn_.items!, + hasReceived, + "Can't cancel a return which has returned items" + ) + } +) + +export const cancelReturnWorkflowId = "cancel-return" +export const cancelReturnWorkflow = createWorkflow( + cancelReturnWorkflowId, + ( + input: WorkflowData + ): WorkflowData => { + const orderReturn: ReturnDTO & { fulfillments: FulfillmentDTO[] } = + useRemoteQueryStep({ + entry_point: "return", + fields: [ + "id", + "items.id", + "items.received_quantity", + "fulfillments.canceled_at", + ], + variables: { id: input.return_id }, + list: false, + throw_if_key_not_found: true, + }) + + validateOrder({ orderReturn, input }) + + cancelOrderReturnStep({ return_id: orderReturn.id }) + } +) diff --git a/packages/core/core-flows/src/order/workflows/create-return.ts b/packages/core/core-flows/src/order/workflows/create-return.ts index 8dd43cc3de..5495eed526 100644 --- a/packages/core/core-flows/src/order/workflows/create-return.ts +++ b/packages/core/core-flows/src/order/workflows/create-return.ts @@ -97,9 +97,13 @@ function prepareShippingMethodData({ returnShippingOption, }: { orderId: string - inputShippingOption: OrderWorkflow.CreateOrderReturnWorkflowInput["return_shipping"] + inputShippingOption?: OrderWorkflow.CreateOrderReturnWorkflowInput["return_shipping"] returnShippingOption: ShippingOptionDTO & WithCalculatedPrice }) { + if (!inputShippingOption) { + return + } + const obj: CreateOrderShippingMethodDTO = { name: returnShippingOption.name, order_id: orderId, @@ -217,7 +221,7 @@ function prepareFulfillmentData({ input: { location_id: locationId, provider_id: returnShippingOption.provider_id, - shipping_option_id: input.return_shipping.option_id, + shipping_option_id: input.return_shipping?.option_id, items: fulfillmentItems, labels: [] as FulfillmentWorkflow.CreateFulfillmentLabelWorkflowDTO[], delivery_address: order.shipping_address ?? ({} as any), // TODO: should it be the stock location address? @@ -235,11 +239,11 @@ function prepareReturnShippingOptionQueryVariables({ region_id?: string } input: { - return_shipping: OrderWorkflow.CreateOrderReturnWorkflowInput["return_shipping"] + return_shipping?: OrderWorkflow.CreateOrderReturnWorkflowInput["return_shipping"] } }) { const variables = { - id: input.return_shipping.option_id, + id: input.return_shipping?.option_id, calculated_price: { context: { currency_code: order.currency_code, diff --git a/packages/core/core-flows/src/order/workflows/index.ts b/packages/core/core-flows/src/order/workflows/index.ts index 61acac5754..9da719aabe 100644 --- a/packages/core/core-flows/src/order/workflows/index.ts +++ b/packages/core/core-flows/src/order/workflows/index.ts @@ -1,6 +1,7 @@ export * from "./archive-orders" export * from "./cancel-order" export * from "./cancel-order-fulfillment" +export * from "./cancel-return" export * from "./complete-orders" export * from "./create-fulfillment" export * from "./create-orders" diff --git a/packages/core/types/src/order/common.ts b/packages/core/types/src/order/common.ts index 0b1c9674f4..19fbe57bf1 100644 --- a/packages/core/types/src/order/common.ts +++ b/packages/core/types/src/order/common.ts @@ -6,7 +6,7 @@ import { BigNumberInput, BigNumberRawValue, BigNumberValue } from "../totals" export type ChangeActionType = | "CANCEL" - | "CANCEL_RETURN" + | "CANCEL_RETURN_ITEM" | "FULFILL_ITEM" | "CANCEL_ITEM_FULFILLMENT" | "ITEM_ADD" @@ -15,8 +15,10 @@ export type ChangeActionType = | "RECEIVE_RETURN_ITEM" | "RETURN_ITEM" | "SHIPPING_ADD" + | "SHIPPING_REMOVE" | "SHIP_ITEM" | "WRITE_OFF_ITEM" + | "REINSTATE_ITEM" export type OrderSummaryDTO = { total: BigNumberValue diff --git a/packages/core/types/src/order/mutations.ts b/packages/core/types/src/order/mutations.ts index ca108fad34..3d6231fdab 100644 --- a/packages/core/types/src/order/mutations.ts +++ b/packages/core/types/src/order/mutations.ts @@ -207,6 +207,9 @@ export interface UpdateOrderLineItemDTO export interface CreateOrderShippingMethodDTO { name: string order_id: string + return_id?: string + claim_id?: string + exchange_id?: string version?: number amount: BigNumberInput shipping_option_id?: string @@ -381,6 +384,7 @@ interface BaseOrderBundledItemActionsDTO { internal_note?: string | null note?: string | null metadata?: Record | null + [key: string]: any } interface BaseOrderBundledActionsDTO { order_id: string @@ -424,6 +428,11 @@ export interface CreateOrderReturnDTO extends BaseOrderBundledActionsDTO { no_notification?: boolean } +export interface CancelOrderReturnDTO + extends Omit { + return_id: string +} + export type OrderClaimType = "refund" | "replace" export type ClaimReason = | "missing_item" @@ -446,17 +455,23 @@ export interface CreateOrderClaimDTO extends BaseOrderBundledActionsDTO { no_notification?: boolean } +export interface CancelOrderClaimDTO + extends Omit { + claim_id: string +} + export interface CreateOrderExchangeDTO extends BaseOrderBundledActionsDTO { additional_items?: BaseOrderBundledItemActionsDTO[] shipping_methods?: Omit[] | string[] - return_shipping: Omit | string + return_shipping?: Omit | string difference_due?: BigNumberInput allow_backorder?: boolean no_notification?: boolean } -export interface CancelOrderReturnDTO { - return_id: string +export interface CancelOrderExchangeDTO + extends Omit { + exchange_id: string } export interface ReceiveOrderReturnDTO diff --git a/packages/core/types/src/order/service.ts b/packages/core/types/src/order/service.ts index 1126c8e664..8122c2bdad 100644 --- a/packages/core/types/src/order/service.ts +++ b/packages/core/types/src/order/service.ts @@ -33,7 +33,10 @@ import { } from "./common" import { CancelOrderChangeDTO, + CancelOrderClaimDTO, + CancelOrderExchangeDTO, CancelOrderFulfillmentDTO, + CancelOrderReturnDTO, ConfirmOrderChangeDTO, CreateOrderAddressDTO, CreateOrderAdjustmentDTO, @@ -1582,12 +1585,10 @@ export interface IOrderModuleService extends IModuleService { sharedContext?: Context ): Promise - /* cancelReturn( - returnData: CancelOrderReturnDTO, + data: CancelOrderReturnDTO, sharedContext?: Context - ): Promise - */ + ): Promise receiveReturn( returnData: ReceiveOrderReturnDTO, @@ -1599,8 +1600,18 @@ export interface IOrderModuleService extends IModuleService { sharedContext?: Context ): Promise + cancelClaim( + data: CancelOrderClaimDTO, + sharedContext?: Context + ): Promise + createExchange( exchangeData: CreateOrderExchangeDTO, sharedContext?: Context ): Promise + + cancelExchange( + data: CancelOrderExchangeDTO, + sharedContext?: Context + ): Promise } diff --git a/packages/core/types/src/workflow/order/cancel-claim.ts b/packages/core/types/src/workflow/order/cancel-claim.ts new file mode 100644 index 0000000000..6dcfe4c6f4 --- /dev/null +++ b/packages/core/types/src/workflow/order/cancel-claim.ts @@ -0,0 +1,4 @@ +export interface CancelOrderClaimWorkflowInput { + claim_id: string + no_notification?: boolean +} diff --git a/packages/core/types/src/workflow/order/cancel-exchange.ts b/packages/core/types/src/workflow/order/cancel-exchange.ts new file mode 100644 index 0000000000..803386c47f --- /dev/null +++ b/packages/core/types/src/workflow/order/cancel-exchange.ts @@ -0,0 +1,4 @@ +export interface CancelOrderExchangeWorkflowInput { + exchange_id: string + no_notification?: boolean +} diff --git a/packages/core/types/src/workflow/order/cancel-return.ts b/packages/core/types/src/workflow/order/cancel-return.ts new file mode 100644 index 0000000000..830d5d9db8 --- /dev/null +++ b/packages/core/types/src/workflow/order/cancel-return.ts @@ -0,0 +1,4 @@ +export interface CancelReturnWorkflowInput { + return_id: string + no_notification?: boolean +} 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 473f9212a9..ff0284c68c 100644 --- a/packages/core/types/src/workflow/order/create-return-order.ts +++ b/packages/core/types/src/workflow/order/create-return-order.ts @@ -13,7 +13,7 @@ export interface CreateOrderReturnWorkflowInput { order_id: string created_by?: string | null // The id of the authenticated user items: CreateReturnItem[] - return_shipping: { + return_shipping?: { option_id: string price?: number } diff --git a/packages/core/types/src/workflow/order/index.ts b/packages/core/types/src/workflow/order/index.ts index ded52e6c2e..1783ad2855 100644 --- a/packages/core/types/src/workflow/order/index.ts +++ b/packages/core/types/src/workflow/order/index.ts @@ -1,5 +1,8 @@ +export * from "./cancel-claim" +export * from "./cancel-exchange" export * from "./cancel-fulfillment" export * from "./cancel-order" +export * from "./cancel-return" export * from "./create-fulfillment" export * from "./create-return-order" export * from "./create-shipment" diff --git a/packages/core/utils/src/link/links.ts b/packages/core/utils/src/link/links.ts index 76205e427c..07b217fd0f 100644 --- a/packages/core/utils/src/link/links.ts +++ b/packages/core/utils/src/link/links.ts @@ -86,6 +86,18 @@ export const LINKS = { Modules.PAYMENT, "payment_collection_id" ), + OrderClaimPaymentCollection: composeLinkName( + Modules.ORDER, + "claim_id", + Modules.PAYMENT, + "payment_collection_id" + ), + OrderExchangePaymentCollection: composeLinkName( + Modules.ORDER, + "exchange_id", + Modules.PAYMENT, + "payment_collection_id" + ), OrderFulfillment: composeLinkName( Modules.ORDER, "order_id", diff --git a/packages/medusa/src/api/admin/returns/[id]/cancel/route.ts b/packages/medusa/src/api/admin/returns/[id]/cancel/route.ts index f8d5e2b5a5..2fbaf9eed3 100644 --- a/packages/medusa/src/api/admin/returns/[id]/cancel/route.ts +++ b/packages/medusa/src/api/admin/returns/[id]/cancel/route.ts @@ -1,35 +1,20 @@ -import { - ContainerRegistrationKeys, - remoteQueryObjectFromString, -} from "@medusajs/utils" +import { cancelReturnWorkflow } from "@medusajs/core-flows" import { AuthenticatedMedusaRequest, MedusaResponse, } from "../../../../../types/routing" +import { AdminPostCancelReturnReqSchemaType } from "../../validators" export const POST = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + const input = req.validatedBody as AdminPostCancelReturnReqSchemaType - const queryObject = remoteQueryObjectFromString({ - entryPoint: "return", - variables: { - filters: { - ...req.filterableFields, - }, - ...req.remoteQueryConfig.pagination, - }, - fields: req.remoteQueryConfig.fields, + const workflow = cancelReturnWorkflow(req.scope) + const { result } = await workflow.run({ + input, }) - const { rows: orders, metadata } = await remoteQuery(queryObject) - - res.json({ - orders, - count: metadata.count, - offset: metadata.skip, - limit: metadata.take, - }) + res.status(200).json({ return: result }) } diff --git a/packages/medusa/src/api/admin/returns/validators.ts b/packages/medusa/src/api/admin/returns/validators.ts index 81fdf7f8e8..a57166ae28 100644 --- a/packages/medusa/src/api/admin/returns/validators.ts +++ b/packages/medusa/src/api/admin/returns/validators.ts @@ -52,7 +52,7 @@ const ItemSchema = z.object({ export const AdminPostReturnsReqSchema = z.object({ order_id: z.string(), items: z.array(ItemSchema), - return_shipping: ReturnShippingSchema, + return_shipping: ReturnShippingSchema.optional(), internal_note: z.string().nullish(), receive_now: z.boolean().optional(), refund_amount: z.number().optional(), @@ -70,3 +70,12 @@ export const AdminPostReceiveReturnsReqSchema = z.object({ export type AdminPostReceiveReturnsReqSchemaType = z.infer< typeof AdminPostReceiveReturnsReqSchema > + +export const AdminPostCancelReturnReqSchema = z.object({ + return_id: z.string(), + no_notification: z.boolean().optional(), + internal_note: z.string().nullish(), +}) +export type AdminPostCancelReturnReqSchemaType = z.infer< + typeof AdminPostReceiveReturnsReqSchema +> diff --git a/packages/modules/link-modules/src/definitions/order-claim-payment-collection.ts b/packages/modules/link-modules/src/definitions/order-claim-payment-collection.ts new file mode 100644 index 0000000000..33db7942a0 --- /dev/null +++ b/packages/modules/link-modules/src/definitions/order-claim-payment-collection.ts @@ -0,0 +1,72 @@ +import { ModuleJoinerConfig } from "@medusajs/types" +import { LINKS, Modules } from "@medusajs/utils" + +export const OrderClaimPaymentCollection: ModuleJoinerConfig = { + serviceName: LINKS.OrderClaimPaymentCollection, + isLink: true, + databaseConfig: { + tableName: "order_claim_payment_collection", + idPrefix: "claimpay", + }, + alias: [ + { + name: [ + "order_claim_payment_collection", + "order_claim_payment_collections", + ], + args: { + entity: "LinkOrderClaimPaymentCollection", + }, + }, + ], + primaryKeys: ["id", "claim_id", "payment_collection_id"], + relationships: [ + { + serviceName: Modules.ORDER, + primaryKey: "id", + foreignKey: "claim_id", + alias: "order", + args: { + methodSuffix: "OrderClaims", + }, + }, + { + serviceName: Modules.PAYMENT, + primaryKey: "id", + foreignKey: "payment_collection_id", + alias: "payment_collection", + args: { + methodSuffix: "PaymentCollections", + }, + }, + ], + extends: [ + { + serviceName: Modules.ORDER, + fieldAlias: { + claim_payment_collections: { + path: "claim_payment_collections_link.payment_collection", + isList: true, + }, + }, + relationship: { + serviceName: LINKS.OrderClaimPaymentCollection, + primaryKey: "claim_id", + foreignKey: "id", + alias: "claim_payment_collections_link", + }, + }, + { + serviceName: Modules.PAYMENT, + fieldAlias: { + claim: "order_claim_link.order", + }, + relationship: { + serviceName: LINKS.OrderClaimPaymentCollection, + primaryKey: "payment_collection_id", + foreignKey: "id", + alias: "order_claim_link", + }, + }, + ], +} diff --git a/packages/modules/link-modules/src/definitions/order-exchange-payment-collection.ts b/packages/modules/link-modules/src/definitions/order-exchange-payment-collection.ts new file mode 100644 index 0000000000..19092d0eae --- /dev/null +++ b/packages/modules/link-modules/src/definitions/order-exchange-payment-collection.ts @@ -0,0 +1,72 @@ +import { ModuleJoinerConfig } from "@medusajs/types" +import { LINKS, Modules } from "@medusajs/utils" + +export const OrderExchangePaymentCollection: ModuleJoinerConfig = { + serviceName: LINKS.OrderExchangePaymentCollection, + isLink: true, + databaseConfig: { + tableName: "order_exchange_payment_collection", + idPrefix: "excpay", + }, + alias: [ + { + name: [ + "order_exchange_payment_collection", + "order_exchange_payment_collections", + ], + args: { + entity: "LinkOrderExchangePaymentCollection", + }, + }, + ], + primaryKeys: ["id", "exchange_id", "payment_collection_id"], + relationships: [ + { + serviceName: Modules.ORDER, + primaryKey: "id", + foreignKey: "exchange_id", + alias: "order", + args: { + methodSuffix: "OrderExchanges", + }, + }, + { + serviceName: Modules.PAYMENT, + primaryKey: "id", + foreignKey: "payment_collection_id", + alias: "payment_collection", + args: { + methodSuffix: "PaymentCollections", + }, + }, + ], + extends: [ + { + serviceName: Modules.ORDER, + fieldAlias: { + exchange_payment_collections: { + path: "exchange_payment_collections_link.payment_collection", + isList: true, + }, + }, + relationship: { + serviceName: LINKS.OrderExchangePaymentCollection, + primaryKey: "exchange_id", + foreignKey: "id", + alias: "exchange_payment_collections_link", + }, + }, + { + serviceName: Modules.PAYMENT, + fieldAlias: { + exchange: "order_exchange_link.order", + }, + relationship: { + serviceName: LINKS.OrderExchangePaymentCollection, + primaryKey: "payment_collection_id", + foreignKey: "id", + alias: "order_exchange_link", + }, + }, + ], +} diff --git a/packages/modules/link-modules/src/definitions/order-payment-collection.ts b/packages/modules/link-modules/src/definitions/order-payment-collection.ts index 59ba12e75a..3fe36fcdc2 100644 --- a/packages/modules/link-modules/src/definitions/order-payment-collection.ts +++ b/packages/modules/link-modules/src/definitions/order-payment-collection.ts @@ -6,7 +6,7 @@ export const OrderPaymentCollection: ModuleJoinerConfig = { isLink: true, databaseConfig: { tableName: "order_payment_collection", - idPrefix: "capaycol", + idPrefix: "ordpay", }, alias: [ { diff --git a/packages/modules/order/integration-tests/__tests__/order-return.ts b/packages/modules/order/integration-tests/__tests__/order-return.ts index e4cfc132bc..a2ab62bdc9 100644 --- a/packages/modules/order/integration-tests/__tests__/order-return.ts +++ b/packages/modules/order/integration-tests/__tests__/order-return.ts @@ -1,6 +1,6 @@ import { CreateOrderDTO, IOrderModuleService } from "@medusajs/types" -import { moduleIntegrationTestRunner } from "medusa-test-utils" import { Modules } from "@medusajs/utils" +import { moduleIntegrationTestRunner } from "medusa-test-utils" jest.setTimeout(1000000) @@ -102,7 +102,7 @@ moduleIntegrationTestRunner({ customer_id: "joe", } as CreateOrderDTO - it("should create an order, fulfill, ship and return the items and cancel some item return", async function () { + it("should create an order, fulfill, ship and return the items", async function () { const createdOrder = await service.createOrders(input) // Fullfilment @@ -477,6 +477,191 @@ moduleIntegrationTestRunner({ }) ) }) + + it("should create an order, fulfill, return the items and cancel some item return", async function () { + const createdOrder = await service.createOrders(input) + + await service.registerFulfillment({ + order_id: createdOrder.id, + items: createdOrder.items!.map((item) => { + return { + id: item.id, + quantity: item.quantity, + } + }), + }) + + const reason = await service.createReturnReasons({ + value: "wrong-size", + label: "Wrong Size", + }) + const reason2 = await service.createReturnReasons({ + value: "disliked", + label: "Disliled", + }) + + const orderReturn = await service.createReturn({ + order_id: createdOrder.id, + reference: Modules.FULFILLMENT, + shipping_method: { + name: "First return method", + amount: 10, + }, + items: createdOrder.items!.map((item) => { + return { + id: item.id, + quantity: 1, + reason_id: reason.id, + } + }), + }) + + const secondReturn = await service.createReturn({ + order_id: createdOrder.id, + reference: Modules.FULFILLMENT, + description: "Return remaining item", + shipping_method: { + name: "Second return method", + amount: 0, + }, + items: [ + { + id: createdOrder.items![1].id, + quantity: 1, + reason_id: reason2.id, + }, + ], + }) + + let getOrder = await service.retrieveOrder(createdOrder.id, { + select: [ + "id", + "version", + "items.id", + "items.quantity", + "items.detail.id", + "items.detail.version", + "items.detail.quantity", + "items.detail.fulfilled_quantity", + "items.detail.return_requested_quantity", + ], + relations: ["items", "items.detail"], + }) + + let serializedOrder = JSON.parse(JSON.stringify(getOrder)) + + expect(serializedOrder).toEqual( + expect.objectContaining({ + items: [ + expect.objectContaining({ + quantity: 1, + detail: expect.objectContaining({ + quantity: 1, + fulfilled_quantity: 1, + return_requested_quantity: 1, + }), + }), + expect.objectContaining({ + quantity: 2, + detail: expect.objectContaining({ + quantity: 2, + fulfilled_quantity: 2, + return_requested_quantity: 2, + }), + }), + expect.objectContaining({ + quantity: 1, + detail: expect.objectContaining({ + quantity: 1, + fulfilled_quantity: 1, + return_requested_quantity: 1, + }), + }), + ], + }) + ) + + // Receive second Return + await service.receiveReturn({ + return_id: secondReturn.id, + internal_note: "received some items", + items: [ + { + id: createdOrder.items![1].id, + quantity: 1, + }, + ], + }) + + // Cancel return + await service.cancelReturn({ + return_id: orderReturn.id, + }) + + await expect( + service.cancelReturn({ return_id: secondReturn.id }) + ).rejects.toThrow( + `Cannot cancel more items than what was requested to return for item ${createdOrder.items[1].id}.` + ) + + getOrder = await service.retrieveOrder(createdOrder.id, { + select: [ + "id", + "version", + "items.id", + "items.quantity", + "items.detail.id", + "items.detail.version", + "items.detail.quantity", + "items.detail.shipped_quantity", + "items.detail.fulfilled_quantity", + "items.detail.return_requested_quantity", + "items.detail.return_received_quantity", + "shipping_methods.id", + ], + relations: ["items", "items.detail"], + }) + + serializedOrder = JSON.parse(JSON.stringify(getOrder)) + + expect(serializedOrder.shipping_methods).toHaveLength(2) + expect(serializedOrder).toEqual( + expect.objectContaining({ + items: [ + expect.objectContaining({ + quantity: 1, + detail: expect.objectContaining({ + quantity: 1, + fulfilled_quantity: 1, + shipped_quantity: 0, + return_requested_quantity: 0, + return_received_quantity: 0, + }), + }), + expect.objectContaining({ + quantity: 2, + detail: expect.objectContaining({ + quantity: 2, + fulfilled_quantity: 2, + shipped_quantity: 0, + return_requested_quantity: 0, + return_received_quantity: 1, + }), + }), + expect.objectContaining({ + quantity: 1, + detail: expect.objectContaining({ + quantity: 1, + fulfilled_quantity: 1, + shipped_quantity: 0, + return_requested_quantity: 0, + return_received_quantity: 0, + }), + }), + ], + }) + ) + }) }) }, }) diff --git a/packages/modules/order/src/models/return.ts b/packages/modules/order/src/models/return.ts index 10119eab55..dd050278fa 100644 --- a/packages/modules/order/src/models/return.ts +++ b/packages/modules/order/src/models/return.ts @@ -25,7 +25,6 @@ import { ReturnItem, Transaction } from "@models" import Claim from "./claim" import Exchange from "./exchange" import Order from "./order" -import OrderItem from "./order-item" import OrderShippingMethod from "./order-shipping-method" type OptionalReturnProps = DAL.EntityDateColumns @@ -131,7 +130,7 @@ export default class Return { @OneToMany(() => ReturnItem, (itemDetail) => itemDetail.return, { cascade: [Cascade.PERSIST], }) - items = new Collection>(this) + items = new Collection>(this) @OneToMany( () => OrderShippingMethod, diff --git a/packages/modules/order/src/services/__tests__/util/actions/returns.ts b/packages/modules/order/src/services/__tests__/util/actions/returns.ts index e7ec6fd7a1..1ae3719056 100644 --- a/packages/modules/order/src/services/__tests__/util/actions/returns.ts +++ b/packages/modules/order/src/services/__tests__/util/actions/returns.ts @@ -78,7 +78,7 @@ describe("Order Return - Actions", function () { actions, }) }).toThrow( - "Cannot request to return more items than what was shipped for item 1." + "Cannot request to return more items than what was fulfilled for item 1." ) expect(() => { @@ -190,7 +190,7 @@ describe("Order Return - Actions", function () { ], }) }).toThrow( - "Cannot request to return more items than what was shipped for item 3." + "Cannot request to return more items than what was fulfilled for item 3." ) expect(() => { diff --git a/packages/modules/order/src/services/actions/cancel-claim.ts b/packages/modules/order/src/services/actions/cancel-claim.ts new file mode 100644 index 0000000000..6eed65d8f7 --- /dev/null +++ b/packages/modules/order/src/services/actions/cancel-claim.ts @@ -0,0 +1,144 @@ +import { + Context, + CreateOrderChangeActionDTO, + OrderTypes, +} from "@medusajs/types" +import { promiseAll } from "@medusajs/utils" +import { ChangeActionType } from "../../utils" + +async function createOrderChange( + service, + data, + returnRef, + actions, + sharedContext +) { + return await service.createOrderChange_( + { + order_id: returnRef.order_id, + claim_id: returnRef.id, + reference: "return", + reference_id: returnRef.id, + description: data.description, + internal_note: data.internal_note, + created_by: data.created_by, + metadata: data.metadata, + actions, + }, + sharedContext + ) +} + +export async function cancelClaim( + this: any, + data: OrderTypes.CancelOrderClaimDTO, + sharedContext?: Context +) { + const claimOrder = await this.retrieveClaim( + data.claim_id, + { + select: [ + "id", + "order_id", + "return.id", + "return.items.id", + "return.items.quantity", + "claim_items.item_id", + "claim_items.quantity", + "additional_items.id", + "additional_items.quantity", + ], + relations: ["return.items", "additional_items", "shipping_methods"], + }, + sharedContext + ) + + const actions: CreateOrderChangeActionDTO[] = [] + + claimOrder.return.items.forEach((item) => { + actions.push({ + action: ChangeActionType.CANCEL_RETURN_ITEM, + order_id: claimOrder.order_id, + claim_id: claimOrder.id, + return_id: claimOrder.return.id, + reference: "return", + reference_id: claimOrder.return.id, + details: { + reference_id: item.id, + order_id: claimOrder.order_id, + claim_id: claimOrder.id, + return_id: claimOrder.return.id, + quantity: item.quantity, + }, + }) + }) + + claimOrder.claim_items.forEach((item) => { + actions.push({ + action: ChangeActionType.REINSTATE_ITEM, + claim_id: claimOrder.id, + reference: "claim", + reference_id: claimOrder.id, + details: { + reference_id: item.id, + claim_id: claimOrder.id, + quantity: item.quantity, + }, + }) + }) + + claimOrder.additional_items.forEach((item) => { + actions.push({ + action: ChangeActionType.ITEM_REMOVE, + order_id: claimOrder.order_id, + claim_id: claimOrder.id, + reference: "claim", + reference_id: claimOrder.id, + details: { + reference_id: item.id, + order_id: claimOrder.order_id, + claim_id: claimOrder.id, + quantity: item.quantity, + }, + }) + }) + + claimOrder.shipping_methods?.forEach((shipping) => { + actions.push({ + action: ChangeActionType.SHIPPING_REMOVE, + order_id: claimOrder.order_id, + claim_id: claimOrder.id, + return_id: claimOrder.return.id, + reference: "claim", + reference_id: shipping.id, + amount: shipping.price, + }) + }) + + const [change] = await createOrderChange( + this, + data, + claimOrder, + actions, + sharedContext + ) + + await promiseAll([ + this.updateClaims( + [ + { + data: { + canceled_at: new Date(), + }, + selector: { + id: claimOrder.id, + }, + }, + ], + sharedContext + ), + this.confirmOrderChange(change.id, sharedContext), + ]) + + return claimOrder +} diff --git a/packages/modules/order/src/services/actions/cancel-exchange.ts b/packages/modules/order/src/services/actions/cancel-exchange.ts new file mode 100644 index 0000000000..2eeaf5a01c --- /dev/null +++ b/packages/modules/order/src/services/actions/cancel-exchange.ts @@ -0,0 +1,128 @@ +import { + Context, + CreateOrderChangeActionDTO, + OrderTypes, +} from "@medusajs/types" +import { promiseAll } from "@medusajs/utils" +import { ChangeActionType } from "../../utils" + +async function createOrderChange( + service, + data, + returnRef, + actions, + sharedContext +) { + return await service.createOrderChange_( + { + order_id: returnRef.order_id, + exchange_id: returnRef.id, + reference: "return", + reference_id: returnRef.id, + description: data.description, + internal_note: data.internal_note, + created_by: data.created_by, + metadata: data.metadata, + actions, + }, + sharedContext + ) +} + +export async function cancelExchange( + this: any, + data: OrderTypes.CancelOrderExchangeDTO, + sharedContext?: Context +) { + const exchangeOrder = await this.retrieveExchange( + data.exchange_id, + { + select: [ + "id", + "order_id", + "return.id", + "return.items.item_id", + "return.items.quantity", + "additional_items.id", + "additional_items.quantity", + ], + relations: ["return.items", "additional_items", "shipping_methods"], + }, + sharedContext + ) + + const actions: CreateOrderChangeActionDTO[] = [] + + exchangeOrder.return.items.forEach((item) => { + actions.push({ + action: ChangeActionType.CANCEL_RETURN_ITEM, + order_id: exchangeOrder.order_id, + exchange_id: exchangeOrder.id, + return_id: exchangeOrder.return.id, + reference: "return", + reference_id: exchangeOrder.return.id, + details: { + reference_id: item.item_id, + order_id: exchangeOrder.order_id, + exchange_id: exchangeOrder.id, + return_id: exchangeOrder.return.id, + quantity: item.quantity, + }, + }) + }) + + exchangeOrder.additional_items.forEach((item) => { + actions.push({ + action: ChangeActionType.ITEM_REMOVE, + order_id: exchangeOrder.order_id, + exchange_id: exchangeOrder.id, + reference: "exchange", + reference_id: exchangeOrder.id, + details: { + order_id: exchangeOrder.order_id, + reference_id: item.item_id, + exchange_id: exchangeOrder.id, + quantity: item.quantity, + }, + }) + }) + + exchangeOrder.shipping_methods?.forEach((shipping) => { + actions.push({ + action: ChangeActionType.SHIPPING_REMOVE, + order_id: exchangeOrder.order_id, + exchange_id: exchangeOrder.id, + return_id: exchangeOrder.return.id, + reference: "exchange", + reference_id: shipping.id, + amount: shipping.price, + }) + }) + + const [change] = await createOrderChange( + this, + data, + exchangeOrder, + actions, + sharedContext + ) + + await promiseAll([ + this.updateExchanges( + [ + { + data: { + canceled_at: new Date(), + }, + selector: { + id: exchangeOrder.id, + }, + }, + ], + sharedContext + ), + this.confirmOrderChange(change.id, sharedContext), + ]) + + return exchangeOrder +} diff --git a/packages/modules/order/src/services/actions/cancel-return.ts b/packages/modules/order/src/services/actions/cancel-return.ts new file mode 100644 index 0000000000..d9e65e55d1 --- /dev/null +++ b/packages/modules/order/src/services/actions/cancel-return.ts @@ -0,0 +1,107 @@ +import { + Context, + CreateOrderChangeActionDTO, + OrderTypes, +} from "@medusajs/types" +import { promiseAll } from "@medusajs/utils" +import { ChangeActionType } from "../../utils" + +async function createOrderChange( + service, + data, + returnRef, + actions, + sharedContext +) { + return await service.createOrderChange_( + { + order_id: returnRef.order_id, + return_id: returnRef.id, + reference: "return", + reference_id: returnRef.id, + description: data.description, + internal_note: data.internal_note, + created_by: data.created_by, + metadata: data.metadata, + actions, + }, + sharedContext + ) +} + +export async function cancelReturn( + this: any, + data: OrderTypes.CancelOrderReturnDTO, + sharedContext?: Context +) { + const returnOrder = await this.retrieveReturn( + data.return_id, + { + select: [ + "id", + "order_id", + "items.item_id", + "items.quantity", + "items.received_quantity", + ], + relations: ["items", "shipping_methods"], + }, + sharedContext + ) + + const actions: CreateOrderChangeActionDTO[] = [] + + returnOrder.items.forEach((item) => { + actions.push({ + action: ChangeActionType.CANCEL_RETURN_ITEM, + order_id: returnOrder.order_id, + return_id: returnOrder.id, + reference: "return", + reference_id: returnOrder.id, + details: { + reference_id: item.item_id, + order_id: returnOrder.order_id, + return_id: returnOrder.id, + quantity: item.quantity, + }, + }) + }) + + returnOrder.shipping_methods?.forEach((shipping) => { + actions.push({ + action: ChangeActionType.SHIPPING_REMOVE, + order_id: returnOrder.order_id, + return_id: returnOrder.id, + reference: "return", + reference_id: shipping.id, + amount: shipping.price, + }) + }) + + const [change] = await createOrderChange( + this, + data, + returnOrder, + actions, + sharedContext + ) + + await promiseAll([ + this.updateReturns( + [ + { + data: { + canceled_at: new Date(), + }, + selector: { + id: returnOrder.id, + }, + }, + ], + sharedContext + ), + this.confirmOrderChange(change.id, sharedContext), + ]) + + return returnOrder +} diff --git a/packages/modules/order/src/services/actions/create-claim.ts b/packages/modules/order/src/services/actions/create-claim.ts index f338dd7b3e..fb818bc2c7 100644 --- a/packages/modules/order/src/services/actions/create-claim.ts +++ b/packages/modules/order/src/services/actions/create-claim.ts @@ -196,9 +196,9 @@ async function processShippingMethods( const methods = await service.createShippingMethods( [ { + ...shippingMethod, order_id: data.order_id, claim_id: claimReference.id, - ...shippingMethod, }, ], sharedContext @@ -247,10 +247,10 @@ async function processReturnShipping( const methods = await service.createShippingMethods( [ { + ...data.return_shipping, order_id: data.order_id, claim_id: claimReference.id, return_id: returnReference.id, - ...data.return_shipping, }, ], sharedContext diff --git a/packages/modules/order/src/services/actions/create-exchange.ts b/packages/modules/order/src/services/actions/create-exchange.ts index bc77eeab52..cd169adb40 100644 --- a/packages/modules/order/src/services/actions/create-exchange.ts +++ b/packages/modules/order/src/services/actions/create-exchange.ts @@ -162,9 +162,9 @@ async function processShippingMethods( const methods = await service.createShippingMethods( [ { + ...shippingMethod, order_id: data.order_id, exchange_id: exchangeReference.id, - ...shippingMethod, }, ], sharedContext @@ -208,10 +208,10 @@ async function processReturnShipping( const methods = await service.createShippingMethods( [ { + ...data.return_shipping, order_id: data.order_id, exchange_id: exchangeReference.id, return_id: returnReference.id, - ...data.return_shipping, }, ], sharedContext diff --git a/packages/modules/order/src/services/actions/create-return.ts b/packages/modules/order/src/services/actions/create-return.ts index 87547ed92b..ab1e4e1314 100644 --- a/packages/modules/order/src/services/actions/create-return.ts +++ b/packages/modules/order/src/services/actions/create-return.ts @@ -6,6 +6,7 @@ import { import { ReturnStatus, getShippingMethodsTotals, + isDefined, isString, promiseAll, } from "@medusajs/utils" @@ -59,13 +60,17 @@ async function processShippingMethod( ) { let shippingMethodId + if (!isDefined(data.shipping_method)) { + return + } + if (!isString(data.shipping_method)) { const methods = await service.createShippingMethods( [ { + ...data.shipping_method, order_id: data.order_id, return_id: returnRef.id, - ...data.shipping_method, }, ], sharedContext @@ -90,8 +95,11 @@ async function processShippingMethod( action: ChangeActionType.SHIPPING_ADD, reference: "order_shipping_method", reference_id: shippingMethodId, - return_id: returnRef.id, amount: calculatedAmount.total, + details: { + order_id: returnRef.order_id, + return_id: returnRef.id, + }, }) } } @@ -134,8 +142,11 @@ export async function createReturn( const em = sharedContext!.transactionManager as any const returnRef = createReturnReference(em, data, order) const actions: CreateOrderChangeActionDTO[] = [] + returnRef.items = createReturnItems(em, data, returnRef, actions) + await processShippingMethod(this, data, returnRef, actions, sharedContext) + const change = await createOrderChange( this, data, diff --git a/packages/modules/order/src/services/actions/index.ts b/packages/modules/order/src/services/actions/index.ts index 640ed6a13a..b721ab9cf6 100644 --- a/packages/modules/order/src/services/actions/index.ts +++ b/packages/modules/order/src/services/actions/index.ts @@ -1,4 +1,7 @@ +export * from "./cancel-claim" +export * from "./cancel-exchange" export * from "./cancel-fulfillment" +export * from "./cancel-return" export * from "./create-claim" export * from "./create-exchange" export * from "./create-return" diff --git a/packages/modules/order/src/services/order-module-service.ts b/packages/modules/order/src/services/order-module-service.ts index 410751d30a..28efc1c929 100644 --- a/packages/modules/order/src/services/order-module-service.ts +++ b/packages/modules/order/src/services/order-module-service.ts @@ -1188,9 +1188,13 @@ export default class OrderModuleService< return { shipping_method: dt, order_id: dt.order_id, + return_id: dt.return_id, + claim_id: dt.claim_id, + exchange_id: dt.exchange_id, version: mapOrderVersion[dt.order_id], } }) + methods = await this.createShippingMethodsBulk_( orderShippingMethodData as any, sharedContext @@ -1214,11 +1218,14 @@ export default class OrderModuleService< sharedContext ) - const methods = data.map((method) => { + const methods = data.map((methodData) => { return { - shipping_method: method, + shipping_method: methodData, order_id: order.id, - version: method.version ?? order.version ?? 1, + return_id: methodData.return_id, + claim_id: methodData.claim_id, + exchange_id: methodData.exchange_id, + version: methodData.version ?? order.version ?? 1, } }) @@ -1952,6 +1959,9 @@ export default class OrderModuleService< select: [ "id", "order_id", + "return_id", + "exchange_id", + "claim_id", "ordering", "version", "applied", @@ -2108,7 +2118,15 @@ export default class OrderModuleService< sharedContext?: Context ): Promise { const options = { - select: ["id", "order_id", "version", "status"], + select: [ + "id", + "order_id", + "return_id", + "claim_id", + "exchange_id", + "version", + "status", + ], relations: [] as string[], order: {}, } @@ -2253,15 +2271,7 @@ export default class OrderModuleService< let orders = await super.listOrders( { id: deduplicate(ordersIds) }, { - select: [ - "id", - "version", - "items.detail", - "transactions", - "shipping_methods", - "summary", - "total", - ], + select: ["id", "version", "items.detail", "summary", "total"], relations: [ "transactions", "items", @@ -2277,7 +2287,7 @@ export default class OrderModuleService< const { itemsToUpsert, - shippingMethodsToInsert, + shippingMethodsToUpsert, summariesToUpsert, orderToUpdate, } = applyChangesToOrder(orders, actionsMap) @@ -2295,9 +2305,9 @@ export default class OrderModuleService< summariesToUpsert.length ? this.orderSummaryService_.upsert(summariesToUpsert, sharedContext) : null, - shippingMethodsToInsert.length - ? this.orderShippingMethodService_.create( - shippingMethodsToInsert, + shippingMethodsToUpsert.length + ? this.orderShippingMethodService_.upsert( + shippingMethodsToUpsert, sharedContext ) : null, @@ -2305,7 +2315,7 @@ export default class OrderModuleService< return { items: itemsToUpsert as any, - shippingMethods: shippingMethodsToInsert as any, + shippingMethods: shippingMethodsToUpsert as any, } } @@ -2803,8 +2813,7 @@ export default class OrderModuleService< async createReturn( data: OrderTypes.CreateOrderReturnDTO, @MedusaContext() sharedContext?: Context - ): Promise { - // TODO: type ReturnDTO + ): Promise { const ret = await BundledActions.createReturn.bind(this)( data, sharedContext @@ -2813,7 +2822,12 @@ export default class OrderModuleService< return await this.retrieveReturn( ret.id, { - relations: ["items"], + relations: [ + "items", + "shipping_methods", + "shipping_methods.tax_lines", + "shipping_methods.adjustments", + ], }, sharedContext ) @@ -2823,11 +2837,16 @@ export default class OrderModuleService< async receiveReturn( data: OrderTypes.ReceiveOrderReturnDTO, @MedusaContext() sharedContext?: Context - ): Promise { + ): Promise { const ret = await this.receiveReturn_(data, sharedContext) return await this.retrieveReturn(ret.id, { - relations: ["items"], + relations: [ + "items", + "shipping_methods", + "shipping_methods.tax_lines", + "shipping_methods.adjustments", + ], }) } @@ -2839,11 +2858,36 @@ export default class OrderModuleService< return await BundledActions.receiveReturn.bind(this)(data, sharedContext) } + @InjectManager("baseRepository_") + async cancelReturn( + data: OrderTypes.CancelOrderReturnDTO, + @MedusaContext() sharedContext?: Context + ): Promise { + const ret = await this.cancelReturn_(data, sharedContext) + + return await this.retrieveReturn(ret.id, { + relations: [ + "items", + "shipping_methods", + "shipping_methods.tax_lines", + "shipping_methods.adjustments", + ], + }) + } + + @InjectTransactionManager("baseRepository_") + private async cancelReturn_( + data: OrderTypes.CancelOrderReturnDTO, + @MedusaContext() sharedContext?: Context + ): Promise { + return await BundledActions.cancelReturn.bind(this)(data, sharedContext) + } + @InjectManager("baseRepository_") async createClaim( data: OrderTypes.CreateOrderClaimDTO, @MedusaContext() sharedContext?: Context - ): Promise { + ): Promise { const ret = await this.createClaim_(data, sharedContext) const claim = await this.retrieveOrderClaim( @@ -2865,7 +2909,7 @@ export default class OrderModuleService< sharedContext ) - return await this.baseRepository_.serialize( + return await this.baseRepository_.serialize( claim, { populate: true, @@ -2874,18 +2918,38 @@ export default class OrderModuleService< } @InjectTransactionManager("baseRepository_") - async createExchange_( - data: OrderTypes.CreateOrderExchangeDTO, + async createClaim_( + data: OrderTypes.CreateOrderClaimDTO, @MedusaContext() sharedContext?: Context ): Promise { - return await BundledActions.createExchange.bind(this)(data, sharedContext) + return await BundledActions.createClaim.bind(this)(data, sharedContext) + } + + @InjectManager("baseRepository_") + async cancelClaim( + data: OrderTypes.CancelOrderClaimDTO, + @MedusaContext() sharedContext?: Context + ): Promise { + const ret = await this.cancelClaim_(data, sharedContext) + + return await this.retrieveOrderClaim(ret.id, { + relations: ["items"], + }) + } + + @InjectTransactionManager("baseRepository_") + private async cancelClaim_( + data: OrderTypes.CancelOrderClaimDTO, + @MedusaContext() sharedContext?: Context + ): Promise { + return await BundledActions.cancelClaim.bind(this)(data, sharedContext) } @InjectManager("baseRepository_") async createExchange( data: OrderTypes.CreateOrderExchangeDTO, @MedusaContext() sharedContext?: Context - ): Promise { + ): Promise { const ret = await this.createExchange_(data, sharedContext) const claim = await this.retrieveOrderExchange( @@ -2905,7 +2969,7 @@ export default class OrderModuleService< sharedContext ) - return await this.baseRepository_.serialize( + return await this.baseRepository_.serialize( claim, { populate: true, @@ -2914,11 +2978,31 @@ export default class OrderModuleService< } @InjectTransactionManager("baseRepository_") - async createClaim_( - data: OrderTypes.CreateOrderClaimDTO, + async createExchange_( + data: OrderTypes.CreateOrderExchangeDTO, @MedusaContext() sharedContext?: Context ): Promise { - return await BundledActions.createClaim.bind(this)(data, sharedContext) + return await BundledActions.createExchange.bind(this)(data, sharedContext) + } + + @InjectManager("baseRepository_") + async cancelExchange( + data: OrderTypes.CancelOrderExchangeDTO, + @MedusaContext() sharedContext?: Context + ): Promise { + const ret = await this.cancelExchange_(data, sharedContext) + + return await this.retrieveOrderExchange(ret.id, { + relations: ["items"], + }) + } + + @InjectTransactionManager("baseRepository_") + private async cancelExchange_( + data: OrderTypes.CancelOrderExchangeDTO, + @MedusaContext() sharedContext?: Context + ): Promise { + return await BundledActions.cancelExchange.bind(this)(data, sharedContext) } @InjectTransactionManager("baseRepository_") diff --git a/packages/modules/order/src/types/shipping-method.ts b/packages/modules/order/src/types/shipping-method.ts index 8069a78317..9fbc13d57b 100644 --- a/packages/modules/order/src/types/shipping-method.ts +++ b/packages/modules/order/src/types/shipping-method.ts @@ -4,6 +4,9 @@ export interface CreateOrderShippingMethodDTO { name: string shipping_option_id?: string order_id: string + return_id?: string + claim_id?: string + exchange_id?: string version?: number amount: BigNumberInput data?: Record diff --git a/packages/modules/order/src/types/utils/index.ts b/packages/modules/order/src/types/utils/index.ts index 4b22823ebc..d87be466d0 100644 --- a/packages/modules/order/src/types/utils/index.ts +++ b/packages/modules/order/src/types/utils/index.ts @@ -32,7 +32,7 @@ export type VirtualOrder = { }[] shipping_methods: { - shipping_method_id: string + id: string order_id: string return_id?: string claim_id?: string @@ -89,14 +89,14 @@ export interface OrderChangeEvent { claim_id?: string exchange_id?: string - group_id?: string + change_id?: string evaluationOnly?: boolean details?: any resolve?: { - group_id?: string + change_id?: string reference_id?: string amount?: BigNumberInput } diff --git a/packages/modules/order/src/utils/action-key.ts b/packages/modules/order/src/utils/action-key.ts index 02e5e5426e..47fe9dbcfb 100644 --- a/packages/modules/order/src/utils/action-key.ts +++ b/packages/modules/order/src/utils/action-key.ts @@ -1,6 +1,5 @@ export enum ChangeActionType { CANCEL = "CANCEL", - CANCEL_RETURN = "CANCEL_RETURN", FULFILL_ITEM = "FULFILL_ITEM", CANCEL_ITEM_FULFILLMENT = "CANCEL_ITEM_FULFILLMENT", ITEM_ADD = "ITEM_ADD", @@ -8,7 +7,10 @@ export enum ChangeActionType { RECEIVE_DAMAGED_RETURN_ITEM = "RECEIVE_DAMAGED_RETURN_ITEM", RECEIVE_RETURN_ITEM = "RECEIVE_RETURN_ITEM", RETURN_ITEM = "RETURN_ITEM", + CANCEL_RETURN_ITEM = "CANCEL_RETURN_ITEM", SHIPPING_ADD = "SHIPPING_ADD", + SHIPPING_REMOVE = "SHIPPING_REMOVE", SHIP_ITEM = "SHIP_ITEM", WRITE_OFF_ITEM = "WRITE_OFF_ITEM", + REINSTATE_ITEM = "REINSTATE_ITEM", } diff --git a/packages/modules/order/src/utils/actions/cancel-return.ts b/packages/modules/order/src/utils/actions/cancel-return.ts index 21c56c7daf..cf79982656 100644 --- a/packages/modules/order/src/utils/actions/cancel-return.ts +++ b/packages/modules/order/src/utils/actions/cancel-return.ts @@ -3,7 +3,7 @@ import { ChangeActionType } from "../action-key" import { OrderChangeProcessing } from "../calculate-order-change" import { setActionReference } from "../set-action-reference" -OrderChangeProcessing.registerActionType(ChangeActionType.CANCEL_RETURN, { +OrderChangeProcessing.registerActionType(ChangeActionType.CANCEL_RETURN_ITEM, { operation({ action, currentOrder }) { const existing = currentOrder.items.find( (item) => item.id === action.details.reference_id diff --git a/packages/modules/order/src/utils/actions/index.ts b/packages/modules/order/src/utils/actions/index.ts index 1e8594775c..e00d9254d0 100644 --- a/packages/modules/order/src/utils/actions/index.ts +++ b/packages/modules/order/src/utils/actions/index.ts @@ -9,4 +9,5 @@ export * from "./receive-return-item" export * from "./return-item" export * from "./ship-item" export * from "./shipping-add" +export * from "./shipping-remove" export * from "./write-off-item" diff --git a/packages/modules/order/src/utils/actions/reinstate-item.ts b/packages/modules/order/src/utils/actions/reinstate-item.ts new file mode 100644 index 0000000000..11fea56a5c --- /dev/null +++ b/packages/modules/order/src/utils/actions/reinstate-item.ts @@ -0,0 +1,64 @@ +import { MathBN, MedusaError, isDefined } from "@medusajs/utils" +import { ChangeActionType } from "../action-key" +import { OrderChangeProcessing } from "../calculate-order-change" +import { setActionReference } from "../set-action-reference" + +OrderChangeProcessing.registerActionType(ChangeActionType.REINSTATE_ITEM, { + operation({ action, currentOrder }) { + const existing = currentOrder.items.find( + (item) => item.id === action.details.reference_id + )! + + existing.detail.written_off_quantity ??= 0 + existing.detail.written_off_quantity = MathBN.sub( + existing.detail.written_off_quantity, + action.details.quantity + ) + + setActionReference(existing, action) + }, + revert({ action, currentOrder }) { + const existing = currentOrder.items.find( + (item) => item.id === action.details.reference_id + )! + + existing.detail.written_off_quantity = MathBN.add( + existing.detail.written_off_quantity, + action.details.quantity + ) + }, + validate({ action, currentOrder }) { + const refId = action.details?.reference_id + if (!isDefined(refId)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Details reference ID is required." + ) + } + + const existing = currentOrder.items.find((item) => item.id === refId) + + if (!existing) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Item ID "${refId}" not found.` + ) + } + + if (!action.details?.quantity) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Quantity to reinstate item ${refId} is required.` + ) + } + + const quantityAvailable = existing!.quantity ?? 0 + const greater = MathBN.gt(action.details?.quantity, quantityAvailable) + if (greater) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Cannot unclaim more items than what was ordered." + ) + } + }, +}) diff --git a/packages/modules/order/src/utils/actions/return-item.ts b/packages/modules/order/src/utils/actions/return-item.ts index 21451ac784..9bd24ec3ab 100644 --- a/packages/modules/order/src/utils/actions/return-item.ts +++ b/packages/modules/order/src/utils/actions/return-item.ts @@ -57,7 +57,7 @@ OrderChangeProcessing.registerActionType(ChangeActionType.RETURN_ITEM, { } const quantityAvailable = MathBN.sub( - existing!.detail?.shipped_quantity ?? 0, + existing!.detail?.fulfilled_quantity ?? 0, existing!.detail?.return_requested_quantity ?? 0 ) @@ -65,7 +65,7 @@ OrderChangeProcessing.registerActionType(ChangeActionType.RETURN_ITEM, { if (greater) { throw new MedusaError( MedusaError.Types.INVALID_DATA, - `Cannot request to return more items than what was shipped for item ${refId}.` + `Cannot request to return more items than what was fulfilled for item ${refId}.` ) } }, diff --git a/packages/modules/order/src/utils/actions/shipping-add.ts b/packages/modules/order/src/utils/actions/shipping-add.ts index d5914209b8..4f9db4207d 100644 --- a/packages/modules/order/src/utils/actions/shipping-add.ts +++ b/packages/modules/order/src/utils/actions/shipping-add.ts @@ -1,6 +1,7 @@ import { MedusaError, isDefined } from "@medusajs/utils" import { ChangeActionType } from "../action-key" import { OrderChangeProcessing } from "../calculate-order-change" +import { setActionReference } from "../set-action-reference" OrderChangeProcessing.registerActionType(ChangeActionType.SHIPPING_ADD, { operation({ action, currentOrder }) { @@ -8,15 +9,20 @@ OrderChangeProcessing.registerActionType(ChangeActionType.SHIPPING_ADD, { ? currentOrder.shipping_methods : [currentOrder.shipping_methods] - shipping.push({ - shipping_method_id: action.reference_id!, - order_id: currentOrder.id, - return_id: action.return_id, - claim_id: action.claim_id, - exchange_id: action.exchange_id, + const existing = shipping.find((sh) => sh.id === action.reference_id) - price: action.amount as number, - }) + if (existing) { + setActionReference(existing, action) + } else { + shipping.push({ + id: action.reference_id!, + order_id: currentOrder.id, + return_id: action.return_id, + claim_id: action.claim_id, + exchange_id: action.exchange_id, + price: action.amount as number, + }) + } currentOrder.shipping_methods = shipping }, @@ -26,7 +32,7 @@ OrderChangeProcessing.registerActionType(ChangeActionType.SHIPPING_ADD, { : [currentOrder.shipping_methods] const existingIndex = shipping.findIndex( - (item) => item.shipping_method_id === action.reference_id + (item) => item.id === action.reference_id ) if (existingIndex > -1) { diff --git a/packages/modules/order/src/utils/actions/shipping-remove.ts b/packages/modules/order/src/utils/actions/shipping-remove.ts new file mode 100644 index 0000000000..a4e2e9fdb6 --- /dev/null +++ b/packages/modules/order/src/utils/actions/shipping-remove.ts @@ -0,0 +1,56 @@ +import { MedusaError, isDefined } from "@medusajs/utils" +import { ChangeActionType } from "../action-key" +import { OrderChangeProcessing } from "../calculate-order-change" + +OrderChangeProcessing.registerActionType(ChangeActionType.SHIPPING_REMOVE, { + operation({ action, currentOrder }) { + const shipping = Array.isArray(currentOrder.shipping_methods) + ? currentOrder.shipping_methods + : [currentOrder.shipping_methods] + + const existingIndex = shipping.findIndex( + (item) => item.id === action.reference_id + ) + + if (existingIndex > -1) { + shipping.splice(existingIndex, 1) + } + + currentOrder.shipping_methods = shipping + }, + revert({ action, currentOrder }) { + const shipping = Array.isArray(currentOrder.shipping_methods) + ? currentOrder.shipping_methods + : [currentOrder.shipping_methods] + + const existingIndex = shipping.findIndex( + (item) => item.id === action.reference_id + ) + + if (existingIndex > -1) { + shipping.push({ + id: action.reference_id!, + order_id: currentOrder.id, + return_id: action.return_id, + claim_id: action.claim_id, + exchange_id: action.exchange_id, + price: action.amount as number, + }) + } + }, + validate({ action }) { + if (!action.reference_id) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Reference ID is required." + ) + } + + if (!isDefined(action.amount)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Amount is required." + ) + } + }, +}) diff --git a/packages/modules/order/src/utils/apply-order-changes.ts b/packages/modules/order/src/utils/apply-order-changes.ts index a34c897d14..92a7b0c037 100644 --- a/packages/modules/order/src/utils/apply-order-changes.ts +++ b/packages/modules/order/src/utils/apply-order-changes.ts @@ -15,7 +15,7 @@ export function applyChangesToOrder( actionsMap: Record ) { const itemsToUpsert: OrderItem[] = [] - const shippingMethodsToInsert: OrderShippingMethod[] = [] + const shippingMethodsToUpsert: OrderShippingMethod[] = [] const summariesToUpsert: any[] = [] const orderToUpdate: any[] = [] @@ -68,8 +68,9 @@ export function applyChangesToOrder( ...((shippingMethod as any).detail ?? shippingMethod), version, } + delete sm.id - shippingMethodsToInsert.push(sm) + shippingMethodsToUpsert.push(sm) } orderToUpdate.push({ @@ -85,7 +86,7 @@ export function applyChangesToOrder( return { itemsToUpsert, - shippingMethodsToInsert, + shippingMethodsToUpsert, summariesToUpsert, orderToUpdate, } diff --git a/packages/modules/order/src/utils/base-repository-find.ts b/packages/modules/order/src/utils/base-repository-find.ts index 06b1b3b664..7d159aaba2 100644 --- a/packages/modules/order/src/utils/base-repository-find.ts +++ b/packages/modules/order/src/utils/base-repository-find.ts @@ -34,11 +34,18 @@ export function setFindMethods(klass: Constructor, entity: any) { let orderAlias = "o0" if (isRelatedEntity) { + if (!config.options.populate.includes("order.items")) { + config.options.populate.unshift("order.items") + } + // first relation is always order if the entity is not Order + const index = config.options.populate.findIndex((p) => p === "order") + if (index > -1) { + config.options.populate.splice(index, 1) + } + config.options.populate.unshift("order") orderAlias = "o1" - - config.options.populate.unshift("order.items") } let defaultVersion = knex.raw(`"${orderAlias}"."version"`) @@ -105,7 +112,12 @@ export function setFindMethods(klass: Constructor, entity: any) { let orderAlias = "o0" if (isRelatedEntity) { - // first relation is always order if entity is not Order + // first relation is always order if the entity is not Order + const index = config.options.populate.findIndex((p) => p === "order") + if (index > -1) { + config.options.populate.splice(index, 1) + } + config.options.populate.unshift("order") orderAlias = "o1" } diff --git a/packages/modules/order/src/utils/calculate-order-change.ts b/packages/modules/order/src/utils/calculate-order-change.ts index e650968262..f2c46d78da 100644 --- a/packages/modules/order/src/utils/calculate-order-change.ts +++ b/packages/modules/order/src/utils/calculate-order-change.ts @@ -118,10 +118,10 @@ export class OrderChangeProcessing { const amount = MathBN.mult(action.amount!, type.isDeduction ? -1 : 1) - if (action.group_id && !action.evaluationOnly) { - this.groupTotal[action.group_id] ??= 0 - this.groupTotal[action.group_id] = MathBN.add( - this.groupTotal[action.group_id], + if (action.change_id && !action.evaluationOnly) { + this.groupTotal[action.change_id] ??= 0 + this.groupTotal[action.change_id] = MathBN.add( + this.groupTotal[action.change_id], amount ) } @@ -146,7 +146,7 @@ export class OrderChangeProcessing { amount ) } else { - if (!this.isEventDone(action) && !action.group_id) { + if (!this.isEventDone(action) && !action.change_id) { summary.difference_sum = MathBN.add(summary.difference_sum, amount) } summary.current_order_total = MathBN.add( @@ -243,8 +243,8 @@ export class OrderChangeProcessing { if (action.resolve.reference_id) { this.resolveReferences(action) } - const groupId = action.resolve.group_id ?? "__default" - if (action.resolve.group_id) { + const groupId = action.resolve.change_id ?? "__default" + if (action.resolve.change_id) { // resolve all actions in the same group this.resolveGroup(action) } @@ -320,7 +320,7 @@ export class OrderChangeProcessing { const type = OrderChangeProcessing.typeDefinition[actionKey] const actions = this.actionsProcessed[actionKey] for (const action of actions) { - if (!resolve?.group_id || action?.group_id !== resolve.group_id) { + if (!resolve?.change_id || action?.change_id !== resolve.change_id) { continue } diff --git a/packages/modules/order/src/utils/transform-order.ts b/packages/modules/order/src/utils/transform-order.ts index 9ead4af4e3..0424b29bf5 100644 --- a/packages/modules/order/src/utils/transform-order.ts +++ b/packages/modules/order/src/utils/transform-order.ts @@ -5,8 +5,9 @@ import { deduplicate, isDefined, } from "@medusajs/utils" -import { Order, OrderClaim, OrderExchange, Return } from "@models" +// Reshape the order object to match the OrderDTO +// This function is used to format the order object before returning to the main module methods export function formatOrder( order, options: { @@ -20,8 +21,9 @@ export function formatOrder( orders.map((order) => { let mainOrder = order - const isRelatedEntity = options?.entity !== Order + const isRelatedEntity = options?.entity?.name !== "Order" + // If the entity is a related entity, the original order is located in the order property if (isRelatedEntity) { if (!order.order) { return order @@ -48,11 +50,12 @@ export function formatOrder( formatOrderReturn(order.return, mainOrder) } - if (options.entity === OrderClaim) { + const entityName = options.entity.name + if (entityName === "OrderClaim") { formatClaim(order) - } else if (options.entity === OrderExchange) { + } else if (entityName === "OrderExchange") { formatExchange(order) - } else if (options.entity === Return) { + } else if (entityName === "Return") { formatReturn(order) } } @@ -85,9 +88,6 @@ export function formatOrder( } function formatOrderReturn(orderReturn, mainOrder) { - orderReturn.items = orderReturn.items.filter( - (item) => !item.is_additional_item - ) orderReturn.items.forEach((orderItem) => { const item = mainOrder.items?.find((item) => item.id === orderItem.item_id) @@ -154,7 +154,15 @@ function formatReturn(returnOrder) { }) } +// Map the public order model to the repository model format +// As the public responses have a different shape than the repository responses, this function is used to map the public properties to the internal db entities +// e.g "items" is the relation between "line-item" and "order" + "version", The line item itself is in "items.item" +// This helper maps to the correct repository to query the DB, and the function "formatOrder" remap the response to the public shape export function mapRepositoryToOrderModel(config, isRelatedEntity = false) { + if (isRelatedEntity) { + return mapRepositoryToRelatedEntity(config) + } + const conf = { ...config } function replace(obj, type): string[] | undefined { @@ -223,3 +231,36 @@ export function mapRepositoryToOrderModel(config, isRelatedEntity = false) { return conf } + +// This function has the same purpose as "mapRepositoryToOrderModel" but for returns, claims and exchanges +function mapRepositoryToRelatedEntity(config) { + const conf = { ...config } + + function replace(obj, type): string[] | undefined { + if (!isDefined(obj[type])) { + return + } + + return deduplicate( + obj[type].sort().map((rel) => { + if ( + rel.includes("shipping_methods") && + !rel.includes("shipping_methods.shipping_method") + ) { + obj.populate.push("shipping_methods.shipping_method") + + return rel.replace( + "shipping_methods", + "shipping_methods.shipping_method" + ) + } + return rel + }) + ) + } + + conf.options.fields = replace(config.options, "fields") + conf.options.populate = replace(config.options, "populate") + + return conf +}