diff --git a/integration-tests/modules/__tests__/order/workflows/order-change.spec.ts b/integration-tests/modules/__tests__/order/workflows/order-change.spec.ts new file mode 100644 index 0000000000..4cc50ee20d --- /dev/null +++ b/integration-tests/modules/__tests__/order/workflows/order-change.spec.ts @@ -0,0 +1,319 @@ +import { + cancelOrderChangeWorkflow, + cancelOrderChangeWorkflowId, + createOrderChangeWorkflow, + declineOrderChangeWorkflow, + declineOrderChangeWorkflowId, + deleteOrderChangeWorkflow, + deleteOrderChangeWorkflowId, +} from "@medusajs/core-flows" +import { IOrderModuleService, OrderChangeDTO, OrderDTO } from "@medusajs/types" +import { ModuleRegistrationName } from "@medusajs/utils" +import { medusaIntegrationTestRunner } from "medusa-test-utils" +import { createOrderFixture, prepareDataFixtures } from "./__fixtures__" + +jest.setTimeout(50000) + +medusaIntegrationTestRunner({ + env: { MEDUSA_FF_MEDUSA_V2: true }, + testSuite: ({ getContainer }) => { + let container + + beforeAll(() => { + container = getContainer() + }) + + describe("Order change workflows", () => { + let order: OrderDTO + let service: IOrderModuleService + + describe("createOrderChangeWorkflow", () => { + beforeEach(async () => { + const fixtures = await prepareDataFixtures({ + container, + }) + + order = await createOrderFixture({ + container, + product: fixtures.product, + location: fixtures.location, + inventoryItem: fixtures.inventoryItem, + }) + }) + + it("should successfully create an order change", async () => { + const { result } = await createOrderChangeWorkflow(container).run({ + input: { + order_id: order.id, + }, + }) + + expect(result).toEqual( + expect.objectContaining({ + id: expect.any(String), + order_id: order.id, + }) + ) + }) + + it("should throw an error when creating an order change when an active one already exists", async () => { + await createOrderChangeWorkflow(container).run({ + input: { + order_id: order.id, + }, + }) + + const { + errors: [error], + } = await createOrderChangeWorkflow(container).run({ + input: { + order_id: order.id, + }, + throwOnError: false, + }) + + expect(error.error).toEqual( + expect.objectContaining({ + message: `Order (${order.id}) already has an existing active order change`, + }) + ) + }) + }) + + describe("cancelOrderChangeWorkflow", () => { + let orderChange: OrderChangeDTO + + beforeEach(async () => { + const fixtures = await prepareDataFixtures({ + container, + }) + + order = await createOrderFixture({ + container, + product: fixtures.product, + location: fixtures.location, + inventoryItem: fixtures.inventoryItem, + }) + + const { result } = await createOrderChangeWorkflow(container).run({ + input: { order_id: order.id }, + }) + + orderChange = result + service = container.resolve(ModuleRegistrationName.ORDER) + }) + + it("should successfully cancel an order change", async () => { + await cancelOrderChangeWorkflow(container).run({ + input: { + id: orderChange.id, + canceled_by: "test", + }, + }) + + const orderChange2 = await service.retrieveOrderChange(orderChange.id) + + expect(orderChange2).toEqual( + expect.objectContaining({ + id: expect.any(String), + canceled_by: "test", + canceled_at: expect.any(Date), + }) + ) + }) + + it("should rollback to its original state when step throws error", async () => { + const workflow = cancelOrderChangeWorkflow(container) + + workflow.appendAction("throw", cancelOrderChangeWorkflowId, { + invoke: async function failStep() { + throw new Error(`Fail`) + }, + }) + + const { + errors: [error], + } = await workflow.run({ + input: { + id: orderChange.id, + canceled_by: "test", + }, + throwOnError: false, + }) + + expect(error.error).toEqual( + expect.objectContaining({ + message: `Fail`, + }) + ) + + const orderChange2 = await service.retrieveOrderChange(orderChange.id) + + expect(orderChange2).toEqual( + expect.objectContaining({ + id: expect.any(String), + canceled_by: null, + canceled_at: null, + }) + ) + }) + }) + + describe("deleteOrderChangeWorkflow", () => { + let orderChange: OrderChangeDTO + + beforeEach(async () => { + const fixtures = await prepareDataFixtures({ + container, + }) + + order = await createOrderFixture({ + container, + product: fixtures.product, + location: fixtures.location, + inventoryItem: fixtures.inventoryItem, + }) + + const { result } = await createOrderChangeWorkflow(container).run({ + input: { order_id: order.id }, + }) + + orderChange = result + service = container.resolve(ModuleRegistrationName.ORDER) + }) + + it("should successfully delete an order change", async () => { + await deleteOrderChangeWorkflow(container).run({ + input: { id: orderChange.id }, + }) + + const orderChange2 = await service.retrieveOrderChange( + orderChange.id, + { withDeleted: true } + ) + + expect(orderChange2).toEqual( + expect.objectContaining({ + id: orderChange.id, + deleted_at: expect.any(Date), + }) + ) + }) + + it("should rollback to its original state when step throws error", async () => { + const workflow = deleteOrderChangeWorkflow(container) + + workflow.appendAction("throw", deleteOrderChangeWorkflowId, { + invoke: async function failStep() { + throw new Error(`Fail`) + }, + }) + + const { + errors: [error], + } = await workflow.run({ + input: { id: orderChange.id }, + throwOnError: false, + }) + + expect(error.error).toEqual( + expect.objectContaining({ + message: `Fail`, + }) + ) + + const orderChange2 = await service.retrieveOrderChange( + orderChange.id, + { withDeleted: true } + ) + + expect(orderChange2).toEqual( + expect.objectContaining({ + id: orderChange.id, + deleted_at: null, + }) + ) + }) + }) + + describe("declineOrderChangeWorkflow", () => { + let orderChange: OrderChangeDTO + + beforeEach(async () => { + const fixtures = await prepareDataFixtures({ + container, + }) + + order = await createOrderFixture({ + container, + product: fixtures.product, + location: fixtures.location, + inventoryItem: fixtures.inventoryItem, + }) + + const { result } = await createOrderChangeWorkflow(container).run({ + input: { order_id: order.id }, + }) + + orderChange = result + service = container.resolve(ModuleRegistrationName.ORDER) + }) + + it("should successfully decline an order change", async () => { + await declineOrderChangeWorkflow(container).run({ + input: { + id: orderChange.id, + declined_by: "test", + }, + }) + + const orderChange2 = await service.retrieveOrderChange(orderChange.id) + + expect(orderChange2).toEqual( + expect.objectContaining({ + id: expect.any(String), + declined_by: "test", + declined_at: expect.any(Date), + }) + ) + }) + + it("should rollback to its original state when step throws error", async () => { + const workflow = declineOrderChangeWorkflow(container) + + workflow.appendAction("throw", declineOrderChangeWorkflowId, { + invoke: async function failStep() { + throw new Error(`Fail`) + }, + }) + + const { + errors: [error], + } = await workflow.run({ + input: { + id: orderChange.id, + declined_by: "test", + }, + throwOnError: false, + }) + + expect(error.error).toEqual( + expect.objectContaining({ + message: `Fail`, + }) + ) + + const orderChange2 = await service.retrieveOrderChange(orderChange.id) + + expect(orderChange2).toEqual( + expect.objectContaining({ + id: expect.any(String), + declined_by: null, + declined_at: null, + }) + ) + }) + }) + }) + }, +}) diff --git a/packages/core/core-flows/src/order/steps/cancel-order-change.ts b/packages/core/core-flows/src/order/steps/cancel-order-change.ts new file mode 100644 index 0000000000..5094785206 --- /dev/null +++ b/packages/core/core-flows/src/order/steps/cancel-order-change.ts @@ -0,0 +1,45 @@ +import { + CancelOrderChangeDTO, + IOrderModuleService, + UpdateOrderChangeDTO, +} from "@medusajs/types" +import { + getSelectsAndRelationsFromObjectArray, + ModuleRegistrationName, +} from "@medusajs/utils" +import { createStep, StepResponse } from "@medusajs/workflows-sdk" + +export const cancelOrderChangeStepId = "cancel-order-change" +export const cancelOrderChangeStep = createStep( + cancelOrderChangeStepId, + async (data: CancelOrderChangeDTO, { container }) => { + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + const { selects, relations } = getSelectsAndRelationsFromObjectArray( + [data], + { objectFields: ["metadata"] } + ) + + const dataBeforeUpdate = await service.retrieveOrderChange(data.id, { + select: [...selects, "canceled_at"], + relations, + }) + + await service.cancelOrderChange(data) + + return new StepResponse(void 0, dataBeforeUpdate) + }, + async (rollbackData, { container }) => { + if (!rollbackData) { + return + } + + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + await service.updateOrderChanges(rollbackData as UpdateOrderChangeDTO) + } +) diff --git a/packages/core/core-flows/src/order/steps/decline-order-change.ts b/packages/core/core-flows/src/order/steps/decline-order-change.ts new file mode 100644 index 0000000000..68b6e8dda6 --- /dev/null +++ b/packages/core/core-flows/src/order/steps/decline-order-change.ts @@ -0,0 +1,45 @@ +import { + DeclineOrderChangeDTO, + IOrderModuleService, + UpdateOrderChangeDTO, +} from "@medusajs/types" +import { + getSelectsAndRelationsFromObjectArray, + ModuleRegistrationName, +} from "@medusajs/utils" +import { createStep, StepResponse } from "@medusajs/workflows-sdk" + +export const declineOrderChangeStepId = "decline-order-change" +export const declineOrderChangeStep = createStep( + declineOrderChangeStepId, + async (data: DeclineOrderChangeDTO, { container }) => { + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + const { selects, relations } = getSelectsAndRelationsFromObjectArray( + [data], + { objectFields: ["metadata"] } + ) + + const dataBeforeUpdate = await service.retrieveOrderChange(data.id, { + select: [...selects, "declined_at"], + relations, + }) + + await service.declineOrderChange(data) + + return new StepResponse(void 0, dataBeforeUpdate) + }, + async (rollbackData, { container }) => { + if (!rollbackData) { + return + } + + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + await service.updateOrderChanges(rollbackData as UpdateOrderChangeDTO) + } +) diff --git a/packages/core/core-flows/src/order/steps/delete-order-change.ts b/packages/core/core-flows/src/order/steps/delete-order-change.ts new file mode 100644 index 0000000000..c01641ae3a --- /dev/null +++ b/packages/core/core-flows/src/order/steps/delete-order-change.ts @@ -0,0 +1,28 @@ +import { IOrderModuleService } from "@medusajs/types" +import { ModuleRegistrationName } from "@medusajs/utils" +import { createStep, StepResponse } from "@medusajs/workflows-sdk" + +export const deleteOrderChangeStepId = "delete-order-change" +export const deleteOrderChangeStep = createStep( + deleteOrderChangeStepId, + async (data: { id: string }, { container }) => { + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + const deleted = await service.softDeleteOrderChanges(data.id) + + return new StepResponse(deleted, data.id) + }, + async (id, { container }) => { + if (!id) { + return + } + + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + await service.restoreOrderChanges(id) + } +) diff --git a/packages/core/core-flows/src/order/steps/index.ts b/packages/core/core-flows/src/order/steps/index.ts index 299b735019..526a4891d9 100644 --- a/packages/core/core-flows/src/order/steps/index.ts +++ b/packages/core/core-flows/src/order/steps/index.ts @@ -1,11 +1,14 @@ export * from "./archive-orders" export * from "./cancel-claim" export * from "./cancel-exchange" +export * from "./cancel-order-change" export * from "./cancel-orders" export * from "./cancel-return" export * from "./complete-orders" export * from "./create-order-change" export * from "./create-orders" +export * from "./decline-order-change" +export * from "./delete-order-change" export * from "./get-item-tax-lines" export * from "./register-fulfillment" export * from "./register-shipment" diff --git a/packages/core/core-flows/src/order/workflows/cancel-order-change.ts b/packages/core/core-flows/src/order/workflows/cancel-order-change.ts new file mode 100644 index 0000000000..752f8c99f2 --- /dev/null +++ b/packages/core/core-flows/src/order/workflows/cancel-order-change.ts @@ -0,0 +1,11 @@ +import { CancelOrderChangeDTO } from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { cancelOrderChangeStep } from "../steps" + +export const cancelOrderChangeWorkflowId = "cancel-order-change" +export const cancelOrderChangeWorkflow = createWorkflow( + cancelOrderChangeWorkflowId, + (input: WorkflowData): WorkflowData => { + cancelOrderChangeStep(input) + } +) diff --git a/packages/core/core-flows/src/order/workflows/decline-order-change.ts b/packages/core/core-flows/src/order/workflows/decline-order-change.ts new file mode 100644 index 0000000000..6f244581a3 --- /dev/null +++ b/packages/core/core-flows/src/order/workflows/decline-order-change.ts @@ -0,0 +1,11 @@ +import { DeclineOrderChangeDTO } from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { declineOrderChangeStep } from "../steps" + +export const declineOrderChangeWorkflowId = "decline-order-change" +export const declineOrderChangeWorkflow = createWorkflow( + declineOrderChangeWorkflowId, + (input: WorkflowData): WorkflowData => { + declineOrderChangeStep(input) + } +) diff --git a/packages/core/core-flows/src/order/workflows/delete-order-change.ts b/packages/core/core-flows/src/order/workflows/delete-order-change.ts new file mode 100644 index 0000000000..5194879f44 --- /dev/null +++ b/packages/core/core-flows/src/order/workflows/delete-order-change.ts @@ -0,0 +1,10 @@ +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { deleteOrderChangeStep } from "../steps" + +export const deleteOrderChangeWorkflowId = "delete-order-change" +export const deleteOrderChangeWorkflow = createWorkflow( + deleteOrderChangeWorkflowId, + (input: WorkflowData<{ id: string }>): WorkflowData => { + deleteOrderChangeStep(input) + } +) diff --git a/packages/core/core-flows/src/order/workflows/index.ts b/packages/core/core-flows/src/order/workflows/index.ts index 2d1790b63e..44a29e66e4 100644 --- a/packages/core/core-flows/src/order/workflows/index.ts +++ b/packages/core/core-flows/src/order/workflows/index.ts @@ -1,5 +1,6 @@ export * from "./archive-orders" export * from "./cancel-order" +export * from "./cancel-order-change" export * from "./cancel-order-fulfillment" export * from "./cancel-return" export * from "./complete-orders" @@ -8,6 +9,8 @@ export * from "./create-order-change" export * from "./create-orders" export * from "./create-return" export * from "./create-shipment" +export * from "./decline-order-change" +export * from "./delete-order-change" export * from "./get-order-detail" export * from "./get-orders-list" export * from "./receive-return" diff --git a/packages/core/types/src/order/mutations.ts b/packages/core/types/src/order/mutations.ts index 681d71f052..75dc6ddbc0 100644 --- a/packages/core/types/src/order/mutations.ts +++ b/packages/core/types/src/order/mutations.ts @@ -270,14 +270,15 @@ export interface UpdateOrderChangeDTO { status?: string description?: string internal_note?: string | null - requested_by?: string - requested_at?: Date - confirmed_by?: string - confirmed_at?: Date - declined_by?: string - declined_reason?: string - declined_at?: Date - canceled_by?: string + requested_by?: string | null + requested_at?: Date | null + confirmed_by?: string | null + confirmed_at?: Date | null + declined_by?: string | null + declined_reason?: string | null + declined_at?: Date | null + canceled_by?: string | null + canceled_at?: Date | null metadata?: Record | null } diff --git a/packages/core/types/src/order/service.ts b/packages/core/types/src/order/service.ts index 5875e725c7..f2e904ff6e 100644 --- a/packages/core/types/src/order/service.ts +++ b/packages/core/types/src/order/service.ts @@ -59,6 +59,7 @@ import { RegisterOrderFulfillmentDTO, RegisterOrderShipmentDTO, UpdateOrderAddressDTO, + UpdateOrderChangeDTO, UpdateOrderDTO, UpdateOrderItemDTO, UpdateOrderItemWithSelectorDTO, @@ -1075,7 +1076,30 @@ export interface IOrderModuleService extends IModuleService { selector: FilterableOrderShippingMethodTaxLineProps, sharedContext?: Context ): Promise + // Order Change + + /** + * This method retrieves a {return type} by its ID. + * + * @param {string} orderChangeId - The order change ID. + * @param {FindConfig} config - The configurations determining how the order is retrieved. Its properties, such as `select` or `relations`, accept the + * attributes or relations associated with a order. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The retrieved {return type}(s). + * + * @example + * ```typescript + * const result = await orderModuleService.retrieveOrder("orderId123"); + * ``` + * + */ + retrieveOrderChange( + orderChangeId: string, + config?: FindConfig, + sharedContext?: Context + ): Promise + createOrderChange( data: CreateOrderChangeDTO, sharedContext?: Context @@ -1127,6 +1151,93 @@ export interface IOrderModuleService extends IModuleService { sharedContext?: Context ): Promise + updateOrderChanges( + data: UpdateOrderChangeDTO, + sharedContext?: Context + ): Promise + + /** + * This method updates {return type}(s) + * + * @param {UpdateOrderChangeDTO[]} data - The order change to be updated. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated {return type}(s). + * + * @example + * ```typescript + * // Example call to updateOrderChanges + * + * const updateOrderChangesData: UpdateOrderChangeDTO[] = [{ + * id: "orderchange123", + * description: "Change due to customer request" + * }]; + * + * const result = await orderModuleService.updateOrderChanges(updateOrderChangesData); + * ``` + * + */ + updateOrderChanges( + data: UpdateOrderChangeDTO[], + sharedContext?: Context + ): Promise + + /** + * This method updates {return type}(s) + * + * @param {UpdateOrderChangeDTO | UpdateOrderChangeDTO[]} data - The order change d t o | order change to be updated. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated {return type}(s). + * + * @example + * ```typescript + * const result = await orderModuleService.createOrderChange({ + * order_id: "order123", + * description: "Adding new item to the order" + * }); + * ``` + * + */ + updateOrderChanges( + data: UpdateOrderChangeDTO | UpdateOrderChangeDTO[], + sharedContext?: Context + ): Promise + + /** + * This method deletes order change by its ID. + * + * @param {string[]} orderChangeId - The list of {summary} + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} Resolves when {summary} + * + * @example + * ```typescript + * await orderModuleService.deleteOrderChanges(["orderChangeId1", "orderChangeId2"]); + * ``` + * + */ + deleteOrderChanges( + orderChangeId: string[], + sharedContext?: Context + ): Promise + + /** + * This method deletes order change by its ID. + * + * @param {string} orderChangeId - The order's ID. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} Resolves when {summary} + * + * @example + * ```typescript + * await orderModuleService.deleteOrderChanges("orderChangeId"); + * ``` + * + */ + deleteOrderChanges( + orderChangeId: string, + sharedContext?: Context + ): Promise + /** * This method deletes order change by its ID. * @@ -1389,6 +1500,18 @@ export interface IOrderModuleService extends IModuleService { sharedContext?: Context ): Promise + softDeleteOrderChanges( + orderChangeId: string | string[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> + + restoreOrderChanges( + orderChangeId: string | string[], + config?: RestoreReturn, + sharedContext?: Context + ): Promise | void> + /** * This method {summary} * diff --git a/packages/modules/order/src/models/order-change.ts b/packages/modules/order/src/models/order-change.ts index f5422cfac3..1fa271cac7 100644 --- a/packages/modules/order/src/models/order-change.ts +++ b/packages/modules/order/src/models/order-change.ts @@ -211,7 +211,7 @@ export default class OrderChange { columnType: "timestamptz", nullable: true, }) - canceled_at?: Date + canceled_at?: Date | null = null @Property({ onCreate: () => new Date(),