From 122186a78dfd544a5e20d9ec4a388cf5cd80a23b Mon Sep 17 00:00:00 2001 From: "Carlos R. L. Rodrigues" <37986729+carlos-r-l-rodrigues@users.noreply.github.com> Date: Mon, 3 Jun 2024 12:31:33 -0300 Subject: [PATCH] chore(order): cancel order (#7586) --- .../__tests__/modules/remote-query.spec.ts | 10 +- .../__tests__/order/draft-order.spec.ts | 1 - .../modules/__tests__/order/order.spec.ts | 1 - .../order/workflows/cancel-order.spec.ts | 436 ++++++++++++++++++ .../workflows/create-fulfillment.spec.ts | 10 +- .../steps/update-fulfillment-workflow.ts | 35 -- .../fulfillment/workflows/create-shipment.ts | 6 +- .../src/order/steps/cancel-orders.ts | 50 ++ .../src/order/steps/complete-orders.ts | 16 +- .../core/core-flows/src/order/steps/index.ts | 1 + .../src/order/utils/order-validation.ts | 2 +- .../src/order/workflows/cancel-order.ts | 130 ++++++ .../core-flows/src/order/workflows/index.ts | 1 + .../src/payment/steps/cancel-payment.ts | 35 ++ .../core-flows/src/payment/steps/index.ts | 2 +- .../delete-reservations-by-line-items.ts | 30 ++ .../reservation/steps/delete-reservations.ts | 2 +- .../core-flows/src/reservation/steps/index.ts | 1 + .../delete-reservations-by-line-items.ts | 14 + .../src/reservation/workflows/index.ts | 1 + .../src/loaders/__tests__/medusa-module.ts | 4 +- .../src/loaders/__tests__/module-loader.ts | 6 +- .../__tests__/joiner/remote-joiner-data.ts | 6 +- .../core/types/src/inventory/service-next.ts | 27 ++ packages/core/types/src/order/common.ts | 4 + packages/core/types/src/order/mutations.ts | 5 + packages/core/types/src/order/service.ts | 10 + .../types/src/workflow/order/cancel-order.ts | 4 + .../core/types/src/workflow/order/index.ts | 1 + .../src/api/admin/orders/[id]/cancel/route.ts | 9 +- .../[fulfillment_id]/cancel/route.ts | 7 +- .../[fulfillment_id]/shipment/route.ts | 7 +- .../admin/orders/[id]/fulfillments/route.ts | 7 +- .../subscribers/order-notifier.ts | 2 +- .../subscribers/__tests__/index.spec.ts | 2 +- .../__tests__/api-key-module-service.spec.ts | 6 +- .../inventory-module-service.spec.ts | 23 +- .../src/services/inventory-level.ts | 15 +- .../inventory-next/src/services/inventory.ts | 69 ++- .../integration-tests/__tests__/order-edit.ts | 12 +- .../src/services/order-module-service.ts | 47 +- .../pricing-module/calculate-price.spec.ts | 4 +- 42 files changed, 945 insertions(+), 116 deletions(-) create mode 100644 integration-tests/modules/__tests__/order/workflows/cancel-order.spec.ts delete mode 100644 packages/core/core-flows/src/fulfillment/steps/update-fulfillment-workflow.ts create mode 100644 packages/core/core-flows/src/order/steps/cancel-orders.ts create mode 100644 packages/core/core-flows/src/order/workflows/cancel-order.ts create mode 100644 packages/core/core-flows/src/payment/steps/cancel-payment.ts create mode 100644 packages/core/core-flows/src/reservation/steps/delete-reservations-by-line-items.ts create mode 100644 packages/core/core-flows/src/reservation/workflows/delete-reservations-by-line-items.ts create mode 100644 packages/core/types/src/workflow/order/cancel-order.ts diff --git a/integration-tests/modules/__tests__/modules/remote-query.spec.ts b/integration-tests/modules/__tests__/modules/remote-query.spec.ts index f762f8b551..3773acb432 100644 --- a/integration-tests/modules/__tests__/modules/remote-query.spec.ts +++ b/integration-tests/modules/__tests__/modules/remote-query.spec.ts @@ -69,7 +69,7 @@ medusaIntegrationTestRunner({ { throwIfKeyNotFound: true } ) - expect(getNonExistingRegion).rejects.toThrow( + await expect(getNonExistingRegion).rejects.toThrow( "region id not found: region_123" ) }) @@ -113,7 +113,7 @@ medusaIntegrationTestRunner({ ]) // Validate all relations, including the link - expect( + await expect( remoteQuery( { region: { @@ -136,7 +136,7 @@ medusaIntegrationTestRunner({ ) // Only validate the relations with Payment. It doesn't fail because the link didn't return any data - expect( + await expect( remoteQuery( { region: { @@ -157,7 +157,7 @@ medusaIntegrationTestRunner({ ).resolves.toHaveLength(1) // The link exists, but the payment doesn't - expect( + await expect( remoteQuery( { region: { @@ -180,7 +180,7 @@ medusaIntegrationTestRunner({ ) // everything is fine - expect( + await expect( remoteQuery( { region: { diff --git a/integration-tests/modules/__tests__/order/draft-order.spec.ts b/integration-tests/modules/__tests__/order/draft-order.spec.ts index ed8d4e8104..e984b6355a 100644 --- a/integration-tests/modules/__tests__/order/draft-order.spec.ts +++ b/integration-tests/modules/__tests__/order/draft-order.spec.ts @@ -25,7 +25,6 @@ jest.setTimeout(50000) const env = { MEDUSA_FF_MEDUSA_V2: true } medusaIntegrationTestRunner({ - debug: true, env, testSuite: ({ dbConnection, getContainer, api }) => { let appContainer diff --git a/integration-tests/modules/__tests__/order/order.spec.ts b/integration-tests/modules/__tests__/order/order.spec.ts index 3a9f84297c..8576522a28 100644 --- a/integration-tests/modules/__tests__/order/order.spec.ts +++ b/integration-tests/modules/__tests__/order/order.spec.ts @@ -25,7 +25,6 @@ jest.setTimeout(50000) const env = { MEDUSA_FF_MEDUSA_V2: true } medusaIntegrationTestRunner({ - debug: true, env, testSuite: ({ dbConnection, getContainer, api }) => { let appContainer diff --git a/integration-tests/modules/__tests__/order/workflows/cancel-order.spec.ts b/integration-tests/modules/__tests__/order/workflows/cancel-order.spec.ts new file mode 100644 index 0000000000..974e2225d0 --- /dev/null +++ b/integration-tests/modules/__tests__/order/workflows/cancel-order.spec.ts @@ -0,0 +1,436 @@ +import { + cancelOrderFulfillmentWorkflow, + cancelOrderWorkflow, + createOrderFulfillmentWorkflow, + createShippingOptionsWorkflow, +} from "@medusajs/core-flows" +import { ModuleRegistrationName, Modules } from "@medusajs/modules-sdk" +import { + FulfillmentWorkflow, + IOrderModuleService, + IRegionModuleService, + IStockLocationServiceNext, + OrderWorkflow, + ProductDTO, + RegionDTO, + ShippingOptionDTO, + StockLocationDTO, +} from "@medusajs/types" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { medusaIntegrationTestRunner } from "medusa-test-utils/dist" + +jest.setTimeout(500000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const providerId = "manual_test-provider" +let inventoryItem + +async function prepareDataFixtures({ container }) { + const fulfillmentService = container.resolve( + ModuleRegistrationName.FULFILLMENT + ) + const salesChannelService = container.resolve( + ModuleRegistrationName.SALES_CHANNEL + ) + const stockLocationModule: IStockLocationServiceNext = container.resolve( + ModuleRegistrationName.STOCK_LOCATION + ) + const productModule = container.resolve(ModuleRegistrationName.PRODUCT) + const inventoryModule = container.resolve(ModuleRegistrationName.INVENTORY) + + const shippingProfile = await fulfillmentService.createShippingProfiles({ + name: "test", + type: "default", + }) + + const fulfillmentSet = await fulfillmentService.create({ + name: "Test fulfillment set", + type: "manual_test", + }) + + const serviceZone = await fulfillmentService.createServiceZones({ + name: "Test service zone", + fulfillment_set_id: fulfillmentSet.id, + geo_zones: [ + { + type: "country", + country_code: "US", + }, + ], + }) + + const regionService = container.resolve( + ModuleRegistrationName.REGION + ) as IRegionModuleService + + const [region] = await regionService.create([ + { + name: "Test region", + currency_code: "eur", + countries: ["fr"], + }, + ]) + + const salesChannel = await salesChannelService.create({ + name: "Webshop", + }) + + const location: StockLocationDTO = await stockLocationModule.create({ + name: "Warehouse", + address: { + address_1: "Test", + city: "Test", + country_code: "US", + postal_code: "12345", + phone: "12345", + }, + }) + + const [product] = await productModule.create([ + { + title: "Test product", + variants: [ + { + title: "Test variant", + sku: "test-variant", + }, + ], + }, + ]) + + inventoryItem = await inventoryModule.create({ + sku: "inv-1234", + }) + + await inventoryModule.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: location.id, + stocked_quantity: 2, + reserved_quantity: 0, + }, + ]) + + const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK) + + await remoteLink.create([ + { + [Modules.STOCK_LOCATION]: { + stock_location_id: location.id, + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: fulfillmentSet.id, + }, + }, + { + [Modules.SALES_CHANNEL]: { + sales_channel_id: salesChannel.id, + }, + [Modules.STOCK_LOCATION]: { + stock_location_id: location.id, + }, + }, + { + [Modules.PRODUCT]: { + variant_id: product.variants[0].id, + }, + [Modules.INVENTORY]: { + inventory_item_id: inventoryItem.id, + }, + }, + ]) + + const shippingOptionData: FulfillmentWorkflow.CreateShippingOptionsWorkflowInput = + { + name: "Shipping option", + price_type: "flat", + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + provider_id: providerId, + type: { + code: "manual-type", + label: "Manual Type", + description: "Manual Type Description", + }, + prices: [ + { + currency_code: "usd", + amount: 10, + }, + { + region_id: region.id, + amount: 100, + }, + ], + } + + const { result } = await createShippingOptionsWorkflow(container).run({ + input: [shippingOptionData], + }) + + const remoteQueryObject = remoteQueryObjectFromString({ + entryPoint: "shipping_option", + variables: { + id: result[0].id, + }, + fields: [ + "id", + "name", + "price_type", + "service_zone_id", + "shipping_profile_id", + "provider_id", + "data", + "metadata", + "type.*", + "created_at", + "updated_at", + "deleted_at", + "shipping_option_type_id", + "prices.*", + ], + }) + + const remoteQuery = container.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const [createdShippingOption] = await remoteQuery(remoteQueryObject) + return { + shippingOption: createdShippingOption, + region, + salesChannel, + location, + product, + } +} + +async function createOrderFixture({ container, product, location }) { + const orderService: IOrderModuleService = container.resolve( + ModuleRegistrationName.ORDER + ) + let order = await orderService.create({ + region_id: "test_region_idclear", + email: "foo@bar.com", + items: [ + { + title: "Custom Item 2", + variant_sku: product.variants[0].sku, + variant_title: product.variants[0].title, + quantity: 1, + unit_price: 50, + adjustments: [ + { + code: "VIP_25 ETH", + amount: "0.000000000000000005", + description: "VIP discount", + promotion_id: "prom_123", + provider_id: "coupon_kings", + }, + ], + } as any, + ], + transactions: [ + { + amount: 50, + currency_code: "usd", + }, + ], + sales_channel_id: "test", + shipping_address: { + first_name: "Test", + last_name: "Test", + address_1: "Test", + city: "Test", + country_code: "US", + postal_code: "12345", + phone: "12345", + }, + billing_address: { + first_name: "Test", + last_name: "Test", + address_1: "Test", + city: "Test", + country_code: "US", + postal_code: "12345", + }, + shipping_methods: [ + { + name: "Test shipping method", + amount: 10, + data: {}, + tax_lines: [ + { + description: "shipping Tax 1", + tax_rate_id: "tax_usa_shipping", + code: "code", + rate: 10, + }, + ], + adjustments: [ + { + code: "VIP_10", + amount: 1, + description: "VIP discount", + promotion_id: "prom_123", + }, + ], + }, + ], + currency_code: "usd", + customer_id: "joe", + }) + + const inventoryModule = container.resolve(ModuleRegistrationName.INVENTORY) + const reservation = await inventoryModule.createReservationItems([ + { + line_item_id: order.items![0].id, + inventory_item_id: inventoryItem.id, + location_id: location.id, + quantity: order.items![0].quantity, + }, + ]) + + order = await orderService.retrieve(order.id, { + relations: ["items"], + }) + + return order +} + +medusaIntegrationTestRunner({ + env, + testSuite: ({ getContainer }) => { + let container + + beforeAll(() => { + container = getContainer() + }) + + describe("Order fulfillment workflow", () => { + let shippingOption: ShippingOptionDTO + let region: RegionDTO + let location: StockLocationDTO + let product: ProductDTO + + let orderService: IOrderModuleService + + beforeEach(async () => { + const fixtures = await prepareDataFixtures({ + container, + }) + + shippingOption = fixtures.shippingOption + region = fixtures.region + location = fixtures.location + product = fixtures.product + + orderService = container.resolve(ModuleRegistrationName.ORDER) + }) + + it("should cancel an order", async () => { + const order = await createOrderFixture({ container, product, location }) + + // Create a fulfillment + const createOrderFulfillmentData: OrderWorkflow.CreateOrderFulfillmentWorkflowInput = + { + order_id: order.id, + created_by: "user_1", + items: [ + { + id: order.items![0].id, + quantity: 1, + }, + ], + no_notification: false, + location_id: undefined, + } + + await createOrderFulfillmentWorkflow(container).run({ + input: createOrderFulfillmentData, + }) + }) + + it("should fail to cancel an order that has fulfilled items", async () => { + const order = await createOrderFixture({ container, product, location }) + + // Create a fulfillment + const createOrderFulfillmentData: OrderWorkflow.CreateOrderFulfillmentWorkflowInput = + { + order_id: order.id, + created_by: "user_1", + items: [ + { + id: order.items![0].id, + quantity: 1, + }, + ], + no_notification: false, + location_id: undefined, + } + + await createOrderFulfillmentWorkflow(container).run({ + input: createOrderFulfillmentData, + }) + + await expect( + cancelOrderWorkflow(container).run({ + input: { + order_id: order.id, + }, + }) + ).rejects.toMatchObject({ + message: + "All fulfillments must be canceled before canceling an order", + }) + + // Cancel the fulfillment + const remoteQuery = container.resolve( + ContainerRegistrationKeys.REMOTE_QUERY + ) + const remoteQueryObject = remoteQueryObjectFromString({ + entryPoint: "order", + variables: { + id: order.id, + }, + fields: ["id", "fulfillments.id"], + }) + + const [orderFulfill] = await remoteQuery(remoteQueryObject) + await cancelOrderFulfillmentWorkflow(container).run({ + input: { + order_id: orderFulfill.id, + fulfillment_id: orderFulfill.fulfillments[0].id, + }, + }) + + await cancelOrderWorkflow(container).run({ + input: { + order_id: order.id, + }, + }) + + const finalOrderQuery = remoteQueryObjectFromString({ + entryPoint: "order", + variables: { + id: order.id, + }, + fields: ["status", "fulfillments.canceled_at"], + }) + const [finalOrder] = await remoteQuery(finalOrderQuery) + + expect(finalOrder).toEqual( + expect.objectContaining({ + status: "canceled", + fulfillments: [ + expect.objectContaining({ + canceled_at: expect.any(Date), + }), + ], + }) + ) + }) + }) + }, +}) diff --git a/integration-tests/modules/__tests__/order/workflows/create-fulfillment.spec.ts b/integration-tests/modules/__tests__/order/workflows/create-fulfillment.spec.ts index fceed4d2c0..384d46e5f9 100644 --- a/integration-tests/modules/__tests__/order/workflows/create-fulfillment.spec.ts +++ b/integration-tests/modules/__tests__/order/workflows/create-fulfillment.spec.ts @@ -17,7 +17,6 @@ import { } from "@medusajs/types" import { ContainerRegistrationKeys, - RuleOperator, remoteQueryObjectFromString, } from "@medusajs/utils" import { medusaIntegrationTestRunner } from "medusa-test-utils/dist" @@ -145,7 +144,7 @@ async function prepareDataFixtures({ container }) { const shippingOptionData: FulfillmentWorkflow.CreateShippingOptionsWorkflowInput = { - name: "Return shipping option", + name: "Shipping option", price_type: "flat", service_zone_id: serviceZone.id, shipping_profile_id: shippingProfile.id, @@ -165,13 +164,6 @@ async function prepareDataFixtures({ container }) { amount: 100, }, ], - rules: [ - { - attribute: "is_return", - operator: RuleOperator.EQ, - value: '"true"', - }, - ], } const { result } = await createShippingOptionsWorkflow(container).run({ diff --git a/packages/core/core-flows/src/fulfillment/steps/update-fulfillment-workflow.ts b/packages/core/core-flows/src/fulfillment/steps/update-fulfillment-workflow.ts deleted file mode 100644 index f261fc3c89..0000000000 --- a/packages/core/core-flows/src/fulfillment/steps/update-fulfillment-workflow.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { FulfillmentWorkflow } from "@medusajs/types" -import { StepResponse, createStep } from "@medusajs/workflows-sdk" -import { updateFulfillmentWorkflow } from "../workflows/update-fulfillment" - -export const updateFulfillmentWorkflowStepId = "update-fulfillment-workflow" -export const updateFulfillmentWorkflowStep = createStep( - updateFulfillmentWorkflowStepId, - async ( - data: FulfillmentWorkflow.UpdateFulfillmentWorkflowInput, - { container } - ) => { - const { - transaction, - result: updated, - errors, - } = await updateFulfillmentWorkflow(container).run({ - input: data, - throwOnError: false, - }) - - if (Array.isArray(errors) && errors[0]) { - throw errors[0].error - } - - return new StepResponse(updated, transaction) - }, - - async (transaction, { container }) => { - if (!transaction) { - return - } - - await updateFulfillmentWorkflow(container).cancel({ transaction }) - } -) diff --git a/packages/core/core-flows/src/fulfillment/workflows/create-shipment.ts b/packages/core/core-flows/src/fulfillment/workflows/create-shipment.ts index 79d980f5df..a687528d50 100644 --- a/packages/core/core-flows/src/fulfillment/workflows/create-shipment.ts +++ b/packages/core/core-flows/src/fulfillment/workflows/create-shipment.ts @@ -5,7 +5,7 @@ import { transform, } from "@medusajs/workflows-sdk" import { validateShipmentStep } from "../steps" -import { updateFulfillmentWorkflowStep } from "../steps/update-fulfillment-workflow" +import { updateFulfillmentWorkflow } from "./update-fulfillment" export const createShipmentWorkflowId = "create-shipment-workflow" export const createShipmentWorkflow = createWorkflow( @@ -20,6 +20,8 @@ export const createShipmentWorkflow = createWorkflow( shipped_at: new Date(), })) - updateFulfillmentWorkflowStep(update) + updateFulfillmentWorkflow.runAsStep({ + input: update, + }) } ) diff --git a/packages/core/core-flows/src/order/steps/cancel-orders.ts b/packages/core/core-flows/src/order/steps/cancel-orders.ts new file mode 100644 index 0000000000..703cbb45d6 --- /dev/null +++ b/packages/core/core-flows/src/order/steps/cancel-orders.ts @@ -0,0 +1,50 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IOrderModuleService } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +type CompleteOrdersStepInput = { + orderIds: string[] +} + +export const cancelOrdersStepId = "cancel-orders" +export const cancelOrdersStep = createStep( + cancelOrdersStepId, + async (data: CompleteOrdersStepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + const orders = await service.list( + { + id: data.orderIds, + }, + { + select: ["id", "status"], + } + ) + + const canceled = await service.cancel(data.orderIds) + return new StepResponse( + canceled, + canceled.map((order) => { + const prevData = orders.find((o) => o.id === order.id)! + return { + id: order.id, + status: prevData.status, + canceled_at: null, + } + }) + ) + }, + async (canceled, { container }) => { + if (!canceled?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + await service.update(canceled) + } +) diff --git a/packages/core/core-flows/src/order/steps/complete-orders.ts b/packages/core/core-flows/src/order/steps/complete-orders.ts index ae049408f5..25431880a5 100644 --- a/packages/core/core-flows/src/order/steps/complete-orders.ts +++ b/packages/core/core-flows/src/order/steps/complete-orders.ts @@ -14,13 +14,23 @@ export const completeOrdersStep = createStep( ModuleRegistrationName.ORDER ) + const orders = await service.list( + { + id: data.orderIds, + }, + { + select: ["id", "status"], + } + ) + const completed = await service.completeOrder(data.orderIds) return new StepResponse( completed, - completed.map((store) => { + completed.map((order) => { + const prevData = orders.find((o) => o.id === order.id)! return { - id: store.id, - status: store.status, + id: order.id, + status: prevData.status, } }) ) diff --git a/packages/core/core-flows/src/order/steps/index.ts b/packages/core/core-flows/src/order/steps/index.ts index 164466f85d..3a86d0b78b 100644 --- a/packages/core/core-flows/src/order/steps/index.ts +++ b/packages/core/core-flows/src/order/steps/index.ts @@ -1,4 +1,5 @@ export * from "./archive-orders" +export * from "./cancel-orders" export * from "./complete-orders" export * from "./create-orders" export * from "./get-item-tax-lines" diff --git a/packages/core/core-flows/src/order/utils/order-validation.ts b/packages/core/core-flows/src/order/utils/order-validation.ts index 84e8ea417c..f312470ec9 100644 --- a/packages/core/core-flows/src/order/utils/order-validation.ts +++ b/packages/core/core-flows/src/order/utils/order-validation.ts @@ -5,7 +5,7 @@ export function throwIfOrderIsCancelled({ order }: { order: OrderDTO }) { if (order.status === OrderStatus.CANCELED) { throw new MedusaError( MedusaError.Types.INVALID_DATA, - `Order with id ${order.id} has been cancelled.` + `Order with id ${order.id} has been canceled.` ) } } diff --git a/packages/core/core-flows/src/order/workflows/cancel-order.ts b/packages/core/core-flows/src/order/workflows/cancel-order.ts new file mode 100644 index 0000000000..f82c450efe --- /dev/null +++ b/packages/core/core-flows/src/order/workflows/cancel-order.ts @@ -0,0 +1,130 @@ +import { + FulfillmentDTO, + OrderDTO, + OrderWorkflow, + PaymentCollectionDTO, +} from "@medusajs/types" +import { MedusaError, deepFlatMap } from "@medusajs/utils" +import { + WorkflowData, + createStep, + createWorkflow, + transform, +} from "@medusajs/workflows-sdk" +import { useRemoteQueryStep } from "../../common" +import { cancelPaymentStep } from "../../payment/steps" +import { deleteReservationsByLineItemsStep } from "../../reservation/steps" +import { cancelOrdersStep } from "../steps/cancel-orders" +import { throwIfOrderIsCancelled } from "../utils/order-validation" + +const validateOrder = createStep( + "validate-order", + ({ + order, + }: { + order: OrderDTO + input: OrderWorkflow.CancelOrderWorkflowInput + }) => { + const order_ = order as OrderDTO & { + payment_collections: PaymentCollectionDTO[] + fulfillments: FulfillmentDTO[] + } + + throwIfOrderIsCancelled({ order }) + + let refunds = 0 + let captures = 0 + + deepFlatMap(order_, "payment_collections.payments", ({ payments }) => { + refunds += payments?.refunds?.length ?? 0 + captures += payments?.captures?.length ?? 0 + }) + + if (captures > 0) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Order with payment capture(s) cannot be canceled" + ) + } + + if (refunds > 0) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Order with payment refund(s) cannot be canceled" + ) + } + + const throwErrorIf = ( + arr: unknown[], + pred: (obj: any) => boolean, + type: string + ) => { + if (arr?.filter(pred).length) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + `All ${type} must be canceled before canceling an order` + ) + } + } + + const notCanceled = (o) => !o.canceled_at + + throwErrorIf(order_.fulfillments, notCanceled, "fulfillments") + /* + TODO: relationship between order and returns, swaps, claims + + throwErrorIf( + order_.returns, + (ret) => ret.status !== "canceled", + "returns" + ) + throwErrorIf(order_.swaps, notCanceled, "swaps") + throwErrorIf(order_.claims, notCanceled, "claims") + */ + } +) + +export const cancelOrderWorkflowId = "cancel-order" +export const cancelOrderWorkflow = createWorkflow( + cancelOrderWorkflowId, + ( + input: WorkflowData + ): WorkflowData => { + const order: OrderDTO & { fulfillments: FulfillmentDTO[] } = + useRemoteQueryStep({ + entry_point: "orders", + fields: [ + "id", + "status", + "items.id", + "fulfillments.canceled_at", + "payment_collections.payments.id", + "payment_collections.payments.refunds.id", + "payment_collections.payments.captures.id", + ], + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }) + + validateOrder({ order, input }) + + const lineItemIds = transform({ order }, ({ order }) => { + return order.items?.map((i) => i.id) + }) + deleteReservationsByLineItemsStep(lineItemIds) + + const paymentIds = transform({ order }, ({ order }) => { + return deepFlatMap( + order, + "payment_collections.payments", + ({ payments }) => { + return payments?.id + } + ) + }) + cancelPaymentStep({ paymentIds }) + + cancelOrdersStep({ orderIds: [order.id] }) + } +) diff --git a/packages/core/core-flows/src/order/workflows/index.ts b/packages/core/core-flows/src/order/workflows/index.ts index 0d4afa98fc..0d4fa15266 100644 --- a/packages/core/core-flows/src/order/workflows/index.ts +++ b/packages/core/core-flows/src/order/workflows/index.ts @@ -1,4 +1,5 @@ export * from "./archive-orders" +export * from "./cancel-order" export * from "./cancel-order-fulfillment" export * from "./complete-orders" export * from "./create-fulfillment" diff --git a/packages/core/core-flows/src/payment/steps/cancel-payment.ts b/packages/core/core-flows/src/payment/steps/cancel-payment.ts new file mode 100644 index 0000000000..a1b34760d8 --- /dev/null +++ b/packages/core/core-flows/src/payment/steps/cancel-payment.ts @@ -0,0 +1,35 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IPaymentModuleService, Logger } from "@medusajs/types" +import { ContainerRegistrationKeys, promiseAll } from "@medusajs/utils" +import { createStep } from "@medusajs/workflows-sdk" + +type StepInput = { + paymentIds: string | string[] +} + +export const cancelPaymentStepId = "cancel-payment-step" +export const cancelPaymentStep = createStep( + cancelPaymentStepId, + async (input: StepInput, { container }) => { + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) + const paymentModule = container.resolve( + ModuleRegistrationName.PAYMENT + ) + + const paymentIds = Array.isArray(input.paymentIds) + ? input.paymentIds + : [input.paymentIds] + + const promises: Promise[] = [] + for (const id of paymentIds) { + promises.push( + paymentModule.cancelPayment(id).catch((e) => { + logger.error( + `Error was thrown trying to cancel payment - ${id} - ${e}` + ) + }) + ) + } + await promiseAll(promises) + } +) diff --git a/packages/core/core-flows/src/payment/steps/index.ts b/packages/core/core-flows/src/payment/steps/index.ts index ab1ba44e2f..6a6e956966 100644 --- a/packages/core/core-flows/src/payment/steps/index.ts +++ b/packages/core/core-flows/src/payment/steps/index.ts @@ -1,3 +1,3 @@ +export * from "./cancel-payment" export * from "./capture-payment" export * from "./refund-payment" - diff --git a/packages/core/core-flows/src/reservation/steps/delete-reservations-by-line-items.ts b/packages/core/core-flows/src/reservation/steps/delete-reservations-by-line-items.ts new file mode 100644 index 0000000000..eba051712b --- /dev/null +++ b/packages/core/core-flows/src/reservation/steps/delete-reservations-by-line-items.ts @@ -0,0 +1,30 @@ +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IInventoryServiceNext } from "@medusajs/types" + +export const deleteReservationsByLineItemsStepId = + "delete-reservations-by-line-items" +export const deleteReservationsByLineItemsStep = createStep( + deleteReservationsByLineItemsStepId, + async (ids: string[], { container }) => { + const service = container.resolve( + ModuleRegistrationName.INVENTORY + ) + + await service.deleteReservationItemsByLineItem(ids) + + return new StepResponse(void 0, ids) + }, + async (prevIds, { container }) => { + if (!prevIds?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.INVENTORY + ) + + await service.restoreReservationItemsByLineItem(prevIds) + } +) diff --git a/packages/core/core-flows/src/reservation/steps/delete-reservations.ts b/packages/core/core-flows/src/reservation/steps/delete-reservations.ts index e0386b264a..c25a4f8f37 100644 --- a/packages/core/core-flows/src/reservation/steps/delete-reservations.ts +++ b/packages/core/core-flows/src/reservation/steps/delete-reservations.ts @@ -1,7 +1,7 @@ import { StepResponse, createStep } from "@medusajs/workflows-sdk" -import { IInventoryServiceNext } from "@medusajs/types" import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IInventoryServiceNext } from "@medusajs/types" export const deleteReservationsStepId = "delete-reservations" export const deleteReservationsStep = createStep( diff --git a/packages/core/core-flows/src/reservation/steps/index.ts b/packages/core/core-flows/src/reservation/steps/index.ts index dca3839772..18071a647a 100644 --- a/packages/core/core-flows/src/reservation/steps/index.ts +++ b/packages/core/core-flows/src/reservation/steps/index.ts @@ -1,3 +1,4 @@ export * from "./create-reservations" export * from "./delete-reservations" +export * from "./delete-reservations-by-line-items" export * from "./update-reservations" diff --git a/packages/core/core-flows/src/reservation/workflows/delete-reservations-by-line-items.ts b/packages/core/core-flows/src/reservation/workflows/delete-reservations-by-line-items.ts new file mode 100644 index 0000000000..28db2903b2 --- /dev/null +++ b/packages/core/core-flows/src/reservation/workflows/delete-reservations-by-line-items.ts @@ -0,0 +1,14 @@ +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" + +import { deleteReservationsByLineItemsStep } from "../steps" + +type WorkflowInput = { ids: string[] } + +export const deleteReservationsByLineItemsWorkflowId = + "delete-reservations-by-line-items" +export const deleteReservationsByLineItemsWorkflow = createWorkflow( + deleteReservationsByLineItemsWorkflowId, + (input: WorkflowData): WorkflowData => { + return deleteReservationsByLineItemsStep(input.ids) + } +) diff --git a/packages/core/core-flows/src/reservation/workflows/index.ts b/packages/core/core-flows/src/reservation/workflows/index.ts index dca3839772..18071a647a 100644 --- a/packages/core/core-flows/src/reservation/workflows/index.ts +++ b/packages/core/core-flows/src/reservation/workflows/index.ts @@ -1,3 +1,4 @@ export * from "./create-reservations" export * from "./delete-reservations" +export * from "./delete-reservations-by-line-items" export * from "./update-reservations" diff --git a/packages/core/modules-sdk/src/loaders/__tests__/medusa-module.ts b/packages/core/modules-sdk/src/loaders/__tests__/medusa-module.ts index e05c4e2fad..01da8f7ee6 100644 --- a/packages/core/modules-sdk/src/loaders/__tests__/medusa-module.ts +++ b/packages/core/modules-sdk/src/loaders/__tests__/medusa-module.ts @@ -280,7 +280,7 @@ describe("Medusa Modules", () => { } as InternalModuleDeclaration, }) - expect(moduleC).rejects.toThrow( + await expect(moduleC).rejects.toThrow( "Module moduleKey already have a 'main' registered." ) }) @@ -314,7 +314,7 @@ describe("Medusa Modules", () => { } as InternalModuleDeclaration, }) - expect(moduleC).rejects.toThrow( + await expect(moduleC).rejects.toThrow( "Module moduleKey already registed as 'module_alias'. Please choose a different alias." ) }) diff --git a/packages/core/modules-sdk/src/loaders/__tests__/module-loader.ts b/packages/core/modules-sdk/src/loaders/__tests__/module-loader.ts index bee4a65af8..1a2d7941ca 100644 --- a/packages/core/modules-sdk/src/loaders/__tests__/module-loader.ts +++ b/packages/core/modules-sdk/src/loaders/__tests__/module-loader.ts @@ -110,7 +110,7 @@ describe("modules loader", () => { }, } - expect( + await expect( moduleLoader({ container, moduleResolutions, logger }) ).rejects.toThrow("Loaders for module TestService failed: loader") }) @@ -136,7 +136,7 @@ describe("modules loader", () => { }, } - expect( + await expect( moduleLoader({ container, moduleResolutions, logger }) ).rejects.toThrow( "No service found in module TestService. Make sure your module exports a service." @@ -165,7 +165,7 @@ describe("modules loader", () => { }, } - expect( + await expect( moduleLoader({ container, moduleResolutions, logger }) ).rejects.toThrow( "No service found in module TestService. Make sure your module exports a service." diff --git a/packages/core/orchestration/src/__tests__/joiner/remote-joiner-data.ts b/packages/core/orchestration/src/__tests__/joiner/remote-joiner-data.ts index 26d023f6a9..a488844ee4 100644 --- a/packages/core/orchestration/src/__tests__/joiner/remote-joiner-data.ts +++ b/packages/core/orchestration/src/__tests__/joiner/remote-joiner-data.ts @@ -793,7 +793,7 @@ describe("RemoteJoiner", () => { fields: ["id", "name", "email"], } - expect(newJoiner.query(queryWithAlias)).rejects.toThrowError( + await expect(newJoiner.query(queryWithAlias)).rejects.toThrowError( `Service with alias "user" was not found.` ) }) @@ -825,6 +825,8 @@ describe("RemoteJoiner", () => { throwIfKeyNotFound: true, }) - expect(dataNotFound).rejects.toThrowError("order id not found: ord_1234556") + await expect(dataNotFound).rejects.toThrowError( + "order id not found: ord_1234556" + ) }) }) diff --git a/packages/core/types/src/inventory/service-next.ts b/packages/core/types/src/inventory/service-next.ts index 4509eef829..e57df93213 100644 --- a/packages/core/types/src/inventory/service-next.ts +++ b/packages/core/types/src/inventory/service-next.ts @@ -733,6 +733,33 @@ export interface IInventoryServiceNext extends IModuleService { context?: Context ): Promise + /** + * This method is used to restore the reservation items associated with a line item or multiple line items that were deleted. + * + * @param {string | string[]} lineItemId - The ID(s) of the line item(s). + * @param {SharedContext} context - A context used to share re9sources, such as transaction manager, between the application and the module. + * @returns {Promise} Resolves when the reservation items are successfully deleted. + * + * @example + * import { + * initialize as initializeInventoryModule, + * } from "@medusajs/inventory" + * + * async function restoreReservationItemsByLineItem ( + * lineItemIds: string[] + * ) { + * const inventoryModule = await initializeInventoryModule({}) + * + * await inventoryModule.restoreReservationItemsByLineItem( + * lineItemIds + * ) + * } + */ + restoreReservationItemsByLineItem( + lineItemId: string | string[], + context?: Context + ): Promise + /** * This method deletes reservation items by their IDs. * diff --git a/packages/core/types/src/order/common.ts b/packages/core/types/src/order/common.ts index fa85a6dbbc..5c28a4b4bd 100644 --- a/packages/core/types/src/order/common.ts +++ b/packages/core/types/src/order/common.ts @@ -875,6 +875,10 @@ export interface OrderDTO { * Holds custom data in key-value pairs. */ metadata?: Record | null + /** + * When the order was canceled. + */ + canceled_at?: string | Date /** * When the order was created. */ diff --git a/packages/core/types/src/order/mutations.ts b/packages/core/types/src/order/mutations.ts index b4d2ee6312..992ac61a0b 100644 --- a/packages/core/types/src/order/mutations.ts +++ b/packages/core/types/src/order/mutations.ts @@ -395,6 +395,11 @@ export interface CreateOrderReturnDTO extends BaseOrderBundledActionsDTO { shipping_method: Omit | string } +export interface CancelOrderReturnDTO { + order_id: string + return_id: string +} + export interface ReceiveOrderReturnDTO extends BaseOrderBundledActionsDTO {} /** ORDER bundled action flows */ diff --git a/packages/core/types/src/order/service.ts b/packages/core/types/src/order/service.ts index bead9ab8e2..d2c14abc1e 100644 --- a/packages/core/types/src/order/service.ts +++ b/packages/core/types/src/order/service.ts @@ -1491,6 +1491,9 @@ export interface IOrderModuleService extends IModuleService { completeOrder(orderId: string[], sharedContext?: Context): Promise completeOrder(orderId: string, sharedContext?: Context): Promise + cancel(orderId: string[], sharedContext?: Context): Promise + cancel(orderId: string, sharedContext?: Context): Promise + // Bundled flows registerFulfillment( data: RegisterOrderFulfillmentDTO, @@ -1512,6 +1515,13 @@ export interface IOrderModuleService extends IModuleService { sharedContext?: Context ): Promise + /* + cancelReturn( + returnData: CancelOrderReturnDTO, + sharedContext?: Context + ): Promise + */ + receiveReturn( returnData: ReceiveOrderReturnDTO, sharedContext?: Context diff --git a/packages/core/types/src/workflow/order/cancel-order.ts b/packages/core/types/src/workflow/order/cancel-order.ts new file mode 100644 index 0000000000..dbe45a3f13 --- /dev/null +++ b/packages/core/types/src/workflow/order/cancel-order.ts @@ -0,0 +1,4 @@ +export interface CancelOrderWorkflowInput { + order_id: string + no_notification?: boolean +} diff --git a/packages/core/types/src/workflow/order/index.ts b/packages/core/types/src/workflow/order/index.ts index 26af063cf1..aa311a75b1 100644 --- a/packages/core/types/src/workflow/order/index.ts +++ b/packages/core/types/src/workflow/order/index.ts @@ -1,4 +1,5 @@ export * from "./cancel-fulfillment" +export * from "./cancel-order" export * from "./create-fulfillment" export * from "./create-return-order" export * from "./create-shipment" diff --git a/packages/medusa/src/api/admin/orders/[id]/cancel/route.ts b/packages/medusa/src/api/admin/orders/[id]/cancel/route.ts index 2ceb3fd77f..aa382c22db 100644 --- a/packages/medusa/src/api/admin/orders/[id]/cancel/route.ts +++ b/packages/medusa/src/api/admin/orders/[id]/cancel/route.ts @@ -1,3 +1,4 @@ +import { cancelOrderWorkflow } from "@medusajs/core-flows" import { ContainerRegistrationKeys, remoteQueryObjectFromString, @@ -15,7 +16,13 @@ export const GET = async ( const variables = { id: req.params.id } - // TODO: cancel order - v1.x - packages/medusa/src/api/routes/admin/orders/cancel-order.ts + const input = { + order_id: req.params.id, + } + + await cancelOrderWorkflow(req.scope).run({ + input, + }) const queryObject = remoteQueryObjectFromString({ entryPoint: "order", diff --git a/packages/medusa/src/api/admin/orders/[id]/fulfillments/[fulfillment_id]/cancel/route.ts b/packages/medusa/src/api/admin/orders/[id]/fulfillments/[fulfillment_id]/cancel/route.ts index c494393cfc..474fb6c0a4 100644 --- a/packages/medusa/src/api/admin/orders/[id]/fulfillments/[fulfillment_id]/cancel/route.ts +++ b/packages/medusa/src/api/admin/orders/[id]/fulfillments/[fulfillment_id]/cancel/route.ts @@ -22,15 +22,10 @@ export const POST = async ( order_id: req.params.id, } - const { errors } = await cancelOrderFulfillmentWorkflow(req.scope).run({ + await cancelOrderFulfillmentWorkflow(req.scope).run({ input, - throwOnError: false, }) - if (Array.isArray(errors) && errors[0]) { - throw errors[0].error - } - const queryObject = remoteQueryObjectFromString({ entryPoint: "order", variables, diff --git a/packages/medusa/src/api/admin/orders/[id]/fulfillments/[fulfillment_id]/shipment/route.ts b/packages/medusa/src/api/admin/orders/[id]/fulfillments/[fulfillment_id]/shipment/route.ts index 27499d8b08..cca297a96f 100644 --- a/packages/medusa/src/api/admin/orders/[id]/fulfillments/[fulfillment_id]/shipment/route.ts +++ b/packages/medusa/src/api/admin/orders/[id]/fulfillments/[fulfillment_id]/shipment/route.ts @@ -24,15 +24,10 @@ export const POST = async ( labels: req.validatedBody.labels ?? [], } - const { errors } = await createOrderShipmentWorkflow(req.scope).run({ + await createOrderShipmentWorkflow(req.scope).run({ input, - throwOnError: false, }) - if (Array.isArray(errors) && errors[0]) { - throw errors[0].error - } - const queryObject = remoteQueryObjectFromString({ entryPoint: "order", variables, diff --git a/packages/medusa/src/api/admin/orders/[id]/fulfillments/route.ts b/packages/medusa/src/api/admin/orders/[id]/fulfillments/route.ts index 67dece10e5..453d4cbb17 100644 --- a/packages/medusa/src/api/admin/orders/[id]/fulfillments/route.ts +++ b/packages/medusa/src/api/admin/orders/[id]/fulfillments/route.ts @@ -22,15 +22,10 @@ export const POST = async ( order_id: req.params.id, } - const { errors } = await createOrderFulfillmentWorkflow(req.scope).run({ + await createOrderFulfillmentWorkflow(req.scope).run({ input, - throwOnError: false, }) - if (Array.isArray(errors) && errors[0]) { - throw errors[0].error - } - const queryObject = remoteQueryObjectFromString({ entryPoint: "order", variables, diff --git a/packages/medusa/src/loaders/helpers/subscribers/__fixtures__/subscribers/order-notifier.ts b/packages/medusa/src/loaders/helpers/subscribers/__fixtures__/subscribers/order-notifier.ts index 9c5f6531b3..1a0c2e5af4 100644 --- a/packages/medusa/src/loaders/helpers/subscribers/__fixtures__/subscribers/order-notifier.ts +++ b/packages/medusa/src/loaders/helpers/subscribers/__fixtures__/subscribers/order-notifier.ts @@ -13,6 +13,6 @@ export default async function orderNotifier({ } export const config: SubscriberConfig = { - event: ["order.placed", "order.cancelled", "order.completed"], + event: ["order.placed", "order.canceled", "order.completed"], context: { subscriberId: "order-notifier" }, } diff --git a/packages/medusa/src/loaders/helpers/subscribers/__tests__/index.spec.ts b/packages/medusa/src/loaders/helpers/subscribers/__tests__/index.spec.ts index b05600568e..8c4da9c65c 100644 --- a/packages/medusa/src/loaders/helpers/subscribers/__tests__/index.spec.ts +++ b/packages/medusa/src/loaders/helpers/subscribers/__tests__/index.spec.ts @@ -81,7 +81,7 @@ describe("SubscriberLoader", () => { ) expect(eventBusServiceMock.subscribe).toHaveBeenCalledWith( - "order.cancelled", + "order.canceled", expect.any(Function), { subscriberId: "order-notifier", diff --git a/packages/modules/api-key/integration-tests/__tests__/api-key-module-service.spec.ts b/packages/modules/api-key/integration-tests/__tests__/api-key-module-service.spec.ts index ba888b1ad4..d335df613e 100644 --- a/packages/modules/api-key/integration-tests/__tests__/api-key-module-service.spec.ts +++ b/packages/modules/api-key/integration-tests/__tests__/api-key-module-service.spec.ts @@ -1,11 +1,11 @@ -import crypto from "crypto" import { Modules } from "@medusajs/modules-sdk" import { IApiKeyModuleService } from "@medusajs/types" import { ApiKeyType } from "@medusajs/utils" +import crypto from "crypto" import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils" import { - createSecretKeyFixture, createPublishableKeyFixture, + createSecretKeyFixture, } from "../__fixtures__" jest.setTimeout(100000) @@ -88,7 +88,7 @@ moduleIntegrationTestRunner({ }) it("should only allow creating one active token", async function () { - expect( + await expect( service.create([createSecretKeyFixture, createSecretKeyFixture]) ).rejects.toThrow( "You can only create one secret key at a time. You tried to create 2 secret keys." diff --git a/packages/modules/inventory-next/integration-tests/__tests__/inventory-module-service.spec.ts b/packages/modules/inventory-next/integration-tests/__tests__/inventory-module-service.spec.ts index 974d3a74a9..3e4cbb14dd 100644 --- a/packages/modules/inventory-next/integration-tests/__tests__/inventory-module-service.spec.ts +++ b/packages/modules/inventory-next/integration-tests/__tests__/inventory-module-service.spec.ts @@ -96,7 +96,7 @@ moduleIntegrationTestRunner({ quantity: 3, }) - expect(reserveMoreThanInStock).rejects.toThrow( + await expect(reserveMoreThanInStock).rejects.toThrow( `Not enough stock available for item ${inventoryItem.id} at location location-1` ) @@ -415,7 +415,7 @@ moduleIntegrationTestRunner({ ]) }) - it("deleted reseravation items by line item", async () => { + it("deleted reseravation items by line item and restore them", async () => { const reservationsPreDeleted = await service.listReservationItems({ line_item_id: "line-item-id", }) @@ -440,6 +440,25 @@ moduleIntegrationTestRunner({ }) expect(reservationsPostDeleted).toEqual([]) + + await service.restoreReservationItemsByLineItem("line-item-id") + + const reservationsPostRestored = await service.listReservationItems({ + line_item_id: "line-item-id", + }) + + expect(reservationsPostRestored).toEqual([ + expect.objectContaining({ + location_id: "location-1", + quantity: 2, + line_item_id: "line-item-id", + }), + expect.objectContaining({ + location_id: "location-1", + quantity: 2, + line_item_id: "line-item-id", + }), + ]) }) it("adjusts inventory levels accordingly when removing reservations by line item", async () => { diff --git a/packages/modules/inventory-next/src/services/inventory-level.ts b/packages/modules/inventory-next/src/services/inventory-level.ts index 01360fafa5..9a3825a953 100644 --- a/packages/modules/inventory-next/src/services/inventory-level.ts +++ b/packages/modules/inventory-next/src/services/inventory-level.ts @@ -1,17 +1,8 @@ -import { - Context, - CreateInventoryLevelInput, - DAL, - SharedContext, -} from "@medusajs/types" -import { - InjectTransactionManager, - MedusaContext, - ModulesSdkUtils, -} from "@medusajs/utils" +import { Context } from "@medusajs/types" +import { ModulesSdkUtils } from "@medusajs/utils" -import { InventoryLevel } from "../models/inventory-level" import { InventoryLevelRepository } from "@repositories" +import { InventoryLevel } from "../models/inventory-level" type InjectedDependencies = { inventoryLevelRepository: InventoryLevelRepository diff --git a/packages/modules/inventory-next/src/services/inventory.ts b/packages/modules/inventory-next/src/services/inventory.ts index 8a09fd1bfa..0c7593a510 100644 --- a/packages/modules/inventory-next/src/services/inventory.ts +++ b/packages/modules/inventory-next/src/services/inventory.ts @@ -15,11 +15,11 @@ import { InjectManager, InjectTransactionManager, InventoryEvents, - isDefined, - isString, MedusaContext, MedusaError, ModulesSdkUtils, + isDefined, + isString, partitionArray, promiseAll, } from "@medusajs/utils" @@ -781,6 +781,43 @@ export default class InventoryModuleService< ) } + /** + * Deletes reservation items by line item + * @param lineItemId - the id of the line item associated with the reservation item + * @param context + */ + + @InjectTransactionManager("baseRepository_") + @EmitEvents() + async restoreReservationItemsByLineItem( + lineItemId: string | string[], + @MedusaContext() context: Context = {} + ): Promise { + const reservations: InventoryNext.ReservationItemDTO[] = + await this.listReservationItems({ line_item_id: lineItemId }, {}, context) + + await this.reservationItemService_.restore( + { line_item_id: lineItemId }, + context + ) + + await this.adjustInventoryLevelsForReservationsRestore( + reservations, + context + ) + + context.messageAggregator?.saveRawMessageData( + reservations.map((reservationItem) => ({ + eventName: InventoryEvents.reservation_item_created, + service: this.constructor.name, + action: CommonEvents.CREATED, + object: "reservation-item", + context, + data: { id: reservationItem.id }, + })) + ) + } + /** * Adjusts the inventory level for a given inventory item and location. * @param inventoryItemId - the id of the inventory item @@ -1040,6 +1077,30 @@ export default class InventoryModuleService< reservations: ReservationItemDTO[], context: Context ): Promise { + await this.adjustInventoryLevelsForReservations_( + reservations, + true, + context + ) + } + + private async adjustInventoryLevelsForReservationsRestore( + reservations: ReservationItemDTO[], + context: Context + ): Promise { + await this.adjustInventoryLevelsForReservations_( + reservations, + false, + context + ) + } + + private async adjustInventoryLevelsForReservations_( + reservations: ReservationItemDTO[], + isDelete: boolean, + context: Context + ): Promise { + const multiplier = isDelete ? -1 : 1 const inventoryLevels = await this.ensureInventoryLevels( reservations.map((r) => ({ inventory_item_id: r.inventory_item_id, @@ -1055,8 +1116,8 @@ export default class InventoryModuleService< const inventoryLevelMap = acc.get(curr.inventory_item_id) ?? new Map() const adjustment = inventoryLevelMap.has(curr.location_id) - ? inventoryLevelMap.get(curr.location_id) - curr.quantity - : -curr.quantity + ? inventoryLevelMap.get(curr.location_id) + curr.quantity * multiplier + : curr.quantity * multiplier inventoryLevelMap.set(curr.location_id, adjustment) acc.set(curr.inventory_item_id, inventoryLevelMap) diff --git a/packages/modules/order/integration-tests/__tests__/order-edit.ts b/packages/modules/order/integration-tests/__tests__/order-edit.ts index 5f56f65418..024b42188b 100644 --- a/packages/modules/order/integration-tests/__tests__/order-edit.ts +++ b/packages/modules/order/integration-tests/__tests__/order-edit.ts @@ -410,7 +410,9 @@ moduleIntegrationTestRunner({ confirmed_by: "cx_agent_123", }) - expect(service.confirmOrderChange(orderChange.id)).rejects.toThrowError( + await expect( + service.confirmOrderChange(orderChange.id) + ).rejects.toThrowError( `Order Change cannot be modified: ${orderChange.id}` ) @@ -579,9 +581,9 @@ moduleIntegrationTestRunner({ canceled_by: "cx_agent_123", }) - expect(service.cancelOrderChange(orderChange.id)).rejects.toThrowError( - "Order Change cannot be modified" - ) + await expect( + service.cancelOrderChange(orderChange.id) + ).rejects.toThrowError("Order Change cannot be modified") await service.declineOrderChange({ id: orderChange2.id, @@ -589,7 +591,7 @@ moduleIntegrationTestRunner({ declined_reason: "changed my mind", }) - expect( + await expect( service.declineOrderChange(orderChange2.id) ).rejects.toThrowError("Order Change cannot be modified") diff --git a/packages/modules/order/src/services/order-module-service.ts b/packages/modules/order/src/services/order-module-service.ts index 0515846b25..2b6bd4a0aa 100644 --- a/packages/modules/order/src/services/order-module-service.ts +++ b/packages/modules/order/src/services/order-module-service.ts @@ -2753,10 +2753,55 @@ export default class OrderModuleService< } await this.orderService_.update( + orderIds.map((id) => { + return { + id, + status: OrderStatus.COMPLETED, + } + }), + sharedContext + ) + + return Array.isArray(orderId) ? orders : orders[0] + } + + async cancel( + orderId: string, + sharedContext?: Context + ): Promise + async cancel( + orderId: string[], + sharedContext?: Context + ): Promise + + @InjectTransactionManager("baseRepository_") + async cancel( + orderId: string | string[], + sharedContext?: Context + ): Promise { + const orderIds = Array.isArray(orderId) ? orderId : [orderId] + const orders = await this.list( { id: orderIds, - status: OrderStatus.COMPLETED, }, + {}, + sharedContext + ) + + const canceled_at = new Date() + for (const order of orders) { + order.status = OrderStatus.CANCELED + order.canceled_at = canceled_at + } + + await this.orderService_.update( + orderIds.map((id) => { + return { + id, + status: OrderStatus.CANCELED, + canceled_at, + } + }), sharedContext ) diff --git a/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts b/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts index 78d2b937a7..eef672f134 100644 --- a/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts +++ b/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts @@ -299,7 +299,7 @@ moduleIntegrationTestRunner({ {} ) - expect(result).rejects.toThrow( + await expect(result).rejects.toThrow( "Method calculatePrices requires currency_code in the pricing context" ) @@ -308,7 +308,7 @@ moduleIntegrationTestRunner({ { context: { region_id: "DE" } } ) - expect(result).rejects.toThrow( + await expect(result).rejects.toThrow( "Method calculatePrices requires currency_code in the pricing context" ) })