diff --git a/.changeset/fancy-banks-move.md b/.changeset/fancy-banks-move.md new file mode 100644 index 0000000000..cea1a9c7e7 --- /dev/null +++ b/.changeset/fancy-banks-move.md @@ -0,0 +1,5 @@ +--- +"@medusajs/core-flows": patch +--- + +fix(core-flows): create reservations on draft order conversion to regular order diff --git a/integration-tests/http/__tests__/draft-order/admin/draft-order.spec.ts b/integration-tests/http/__tests__/draft-order/admin/draft-order.spec.ts index 178b0fed00..4e419efc4c 100644 --- a/integration-tests/http/__tests__/draft-order/admin/draft-order.spec.ts +++ b/integration-tests/http/__tests__/draft-order/admin/draft-order.spec.ts @@ -1,7 +1,10 @@ import { medusaIntegrationTestRunner } from "@medusajs/test-utils" import { HttpTypes } from "@medusajs/types" import { ModuleRegistrationName, ProductStatus } from "@medusajs/utils" -import { adminHeaders, createAdminUser, } from "../../../../helpers/create-admin-user" +import { + adminHeaders, + createAdminUser, +} from "../../../../helpers/create-admin-user" import { setupTaxStructure } from "../../../../modules/__tests__/fixtures" jest.setTimeout(300000) @@ -238,6 +241,94 @@ medusaIntegrationTestRunner({ }) describe("POST /draft-orders/:id/convert-to-order", () => { + let product + let inventoryItemLarge + let inventoryItemMedium + + beforeEach(async () => { + inventoryItemLarge = ( + await api.post( + `/admin/inventory-items`, + { sku: "shirt-large" }, + adminHeaders + ) + ).data.inventory_item + + inventoryItemMedium = ( + await api.post( + `/admin/inventory-items`, + { sku: "shirt-medium" }, + adminHeaders + ) + ).data.inventory_item + + await api.post( + `/admin/inventory-items/${inventoryItemLarge.id}/location-levels`, + { + location_id: stockLocation.id, + stocked_quantity: 10, + }, + adminHeaders + ) + + await api.post( + `/admin/inventory-items/${inventoryItemMedium.id}/location-levels`, + { + location_id: stockLocation.id, + stocked_quantity: 10, + }, + adminHeaders + ) + + product = ( + await api.post( + "/admin/products", + { + title: "Shirt", + status: ProductStatus.PUBLISHED, + options: [{ title: "size", values: ["large", "medium"] }], + variants: [ + { + title: "L shirt", + options: { size: "large" }, + manage_inventory: true, + inventory_items: [ + { + inventory_item_id: inventoryItemLarge.id, + required_quantity: 1, + }, + ], + prices: [ + { + currency_code: "usd", + amount: 10, + }, + ], + }, + { + title: "M shirt", + options: { size: "medium" }, + manage_inventory: true, + inventory_items: [ + { + inventory_item_id: inventoryItemMedium.id, + required_quantity: 1, + }, + ], + prices: [ + { + currency_code: "usd", + amount: 10, + }, + ], + }, + ], + }, + adminHeaders + ) + ).data.product + }) + it("should convert a draft order to an order", async () => { const response = await api.post( `/admin/draft-orders/${testDraftOrder.id}/convert-to-order`, @@ -248,6 +339,83 @@ medusaIntegrationTestRunner({ expect(response.status).toBe(200) expect(response.data.order.status).toBe("pending") }) + + it("should create reservations on draft order to order conversion", async () => { + await api.post( + `/admin/draft-orders/${testDraftOrder.id}/edit`, + {}, + adminHeaders + ) + + await api.post( + `/admin/draft-orders/${testDraftOrder.id}/edit/items`, + { + items: [ + { + variant_id: product.variants.find((v) => v.title === "L shirt") + .id, + quantity: 1, + }, + ], + }, + adminHeaders + ) + + await api.post( + `/admin/draft-orders/${testDraftOrder.id}/edit/items`, + { + items: [ + { + variant_id: product.variants.find((v) => v.title === "M shirt") + .id, + quantity: 1, + }, + ], + }, + adminHeaders + ) + + let reservations = (await api.get(`/admin/reservations`, adminHeaders)) + .data.reservations + + expect(reservations.length).toBe(0) + + await api.post( + `/admin/draft-orders/${testDraftOrder.id}/edit/confirm`, + {}, + adminHeaders + ) + + reservations = (await api.get(`/admin/reservations`, adminHeaders)).data + .reservations + + expect(reservations.length).toBe(0) + + const response = await api.post( + `/admin/draft-orders/${testDraftOrder.id}/convert-to-order`, + {}, + adminHeaders + ) + + reservations = (await api.get(`/admin/reservations`, adminHeaders)).data + .reservations + + expect(reservations).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + inventory_item_id: inventoryItemLarge.id, + quantity: 1, + }), + expect.objectContaining({ + inventory_item_id: inventoryItemMedium.id, + quantity: 1, + }), + ]) + ) + + expect(response.status).toBe(200) + expect(response.data.order.status).toBe("pending") + }) }) describe("POST /draft-orders/:id/edit/items/:item_id", () => { @@ -375,7 +543,7 @@ medusaIntegrationTestRunner({ ).data.product }) - it("should manage reservations on order edit", async () => { + it("should not create reservations on draft order edit confirmation", async () => { let reservations = (await api.get(`/admin/reservations`, adminHeaders)) .data.reservations @@ -429,18 +597,7 @@ medusaIntegrationTestRunner({ reservations = (await api.get(`/admin/reservations`, adminHeaders)).data .reservations - expect(reservations).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - inventory_item_id: inventoryItemLarge.id, - quantity: 1, - }), - expect.objectContaining({ - inventory_item_id: inventoryItemMedium.id, - quantity: 1, - }), - ]) - ) + expect(reservations.length).toBe(0) // Create second edit edit = ( @@ -495,19 +652,7 @@ medusaIntegrationTestRunner({ reservations = (await api.get(`/admin/reservations`, adminHeaders)).data .reservations - expect(reservations.length).toBe(2) - expect(reservations).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - inventory_item_id: inventoryItemLarge.id, - quantity: 2, - }), - expect.objectContaining({ - inventory_item_id: inventoryItemSmall.id, - quantity: 1, - }), - ]) - ) + expect(reservations.length).toBe(0) }) }) diff --git a/packages/core/core-flows/src/draft-order/workflows/confirm-draft-order-edit.ts b/packages/core/core-flows/src/draft-order/workflows/confirm-draft-order-edit.ts index 749df1d975..1360bf4343 100644 --- a/packages/core/core-flows/src/draft-order/workflows/confirm-draft-order-edit.ts +++ b/packages/core/core-flows/src/draft-order/workflows/confirm-draft-order-edit.ts @@ -1,15 +1,9 @@ -import { ChangeActionType, MathBN, OrderChangeStatus, } from "@medusajs/framework/utils" -import { createWorkflow, transform, WorkflowResponse, } from "@medusajs/framework/workflows-sdk" -import { BigNumberInput, OrderChangeDTO, OrderDTO, } from "@medusajs/framework/types" -import { reserveInventoryStep } from "../../cart" -import { - prepareConfirmInventoryInput, - requiredOrderFieldsForInventoryConfirmation, -} from "../../cart/utils/prepare-confirm-inventory-input" +import { OrderChangeStatus, } from "@medusajs/framework/utils" +import { createWorkflow, WorkflowResponse, } from "@medusajs/framework/workflows-sdk" +import { OrderChangeDTO, OrderDTO, } from "@medusajs/framework/types" import { useRemoteQueryStep } from "../../common" import { createOrUpdateOrderPaymentCollectionWorkflow, previewOrderChangeStep, } from "../../order" import { confirmOrderChanges } from "../../order/steps/confirm-order-changes" -import { deleteReservationsByLineItemsStep } from "../../reservation" import { validateDraftOrderChangeStep } from "../steps/validate-draft-order-change" import { acquireLockStep, releaseLockStep } from "../../locking" @@ -111,102 +105,6 @@ export const confirmDraftOrderEditWorkflow = createWorkflow( confirmed_by: input.confirmed_by, }) - const orderItems = useRemoteQueryStep({ - entry_point: "order", - fields: requiredOrderFieldsForInventoryConfirmation, - variables: { id: input.order_id }, - list: false, - throw_if_key_not_found: true, - }).config({ name: "order-items-query" }) - - const { variants, items, toRemoveReservationLineItemIds } = transform( - { orderItems, previousOrderItems: order.items, orderPreview }, - ({ orderItems, previousOrderItems, orderPreview }) => { - const allItems: any[] = [] - const allVariants: any[] = [] - - const previousItemIds = (previousOrderItems || []).map(({ id }) => id) - const currentItemIds = orderItems.items.map(({ id }) => id) - - const removedItemIds = previousItemIds.filter( - (id) => !currentItemIds.includes(id) - ) - - const updatedItemIds: string[] = [] - - orderItems.items.forEach((ordItem) => { - const itemAction = orderPreview.items?.find( - (item) => - item.id === ordItem.id && - item.actions?.find( - (a) => - a.action === ChangeActionType.ITEM_ADD || - a.action === ChangeActionType.ITEM_UPDATE - ) - ) - - if (!itemAction) { - return - } - - const unitPrice: BigNumberInput = - itemAction.raw_unit_price ?? itemAction.unit_price - - const compareAtUnitPrice: BigNumberInput | undefined = - itemAction.raw_compare_at_unit_price ?? - itemAction.compare_at_unit_price - - const updateAction = itemAction.actions!.find( - (a) => a.action === ChangeActionType.ITEM_UPDATE - ) - - if (updateAction) { - updatedItemIds.push(ordItem.id) - } - - const newQuantity: BigNumberInput = - itemAction.raw_quantity ?? itemAction.quantity - - const reservationQuantity = MathBN.sub( - newQuantity, - ordItem.raw_fulfilled_quantity - ) - - allItems.push({ - id: ordItem.id, - variant_id: ordItem.variant_id, - quantity: reservationQuantity, - unit_price: unitPrice, - compare_at_unit_price: compareAtUnitPrice, - }) - allVariants.push(ordItem.variant) - }) - - return { - variants: allVariants, - items: allItems, - toRemoveReservationLineItemIds: [ - ...removedItemIds, - ...updatedItemIds, - ], - } - } - ) - - const formatedInventoryItems = transform( - { - input: { - sales_channel_id: (orderItems as any).sales_channel_id, - variants, - items, - }, - }, - prepareConfirmInventoryInput - ) - - deleteReservationsByLineItemsStep(toRemoveReservationLineItemIds) - reserveInventoryStep(formatedInventoryItems) - createOrUpdateOrderPaymentCollectionWorkflow.runAsStep({ input: { order_id: order.id, diff --git a/packages/core/core-flows/src/draft-order/workflows/convert-draft-order.ts b/packages/core/core-flows/src/draft-order/workflows/convert-draft-order.ts index b8758d3828..c7334616a2 100644 --- a/packages/core/core-flows/src/draft-order/workflows/convert-draft-order.ts +++ b/packages/core/core-flows/src/draft-order/workflows/convert-draft-order.ts @@ -8,13 +8,23 @@ import { createWorkflow, parallelize, StepResponse, + transform, WorkflowData, WorkflowResponse, } from "@medusajs/framework/workflows-sdk" -import type { IOrderModuleService, OrderDTO } from "@medusajs/framework/types" +import type { + ConfirmVariantInventoryWorkflowInputDTO, + IOrderModuleService, + OrderDTO, +} from "@medusajs/framework/types" import { emitEventStep, useRemoteQueryStep } from "../../common" import { validateDraftOrderStep } from "../steps/validate-draft-order" import { acquireLockStep, releaseLockStep } from "../../locking" +import { + prepareConfirmInventoryInput, + requiredOrderFieldsForInventoryConfirmation, +} from "../../cart/utils/prepare-confirm-inventory-input" +import { reserveInventoryStep } from "../../cart" export const convertDraftOrderWorkflowId = "convert-draft-order" @@ -119,6 +129,46 @@ export const convertDraftOrderWorkflow = createWorkflow( validateDraftOrderStep({ order }) + const orderItems = useRemoteQueryStep({ + entry_point: "order", + fields: requiredOrderFieldsForInventoryConfirmation, + variables: { id: input.id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "order-items-query" }) + + const { variants, items } = transform({ orderItems }, ({ orderItems }) => { + const items: ConfirmVariantInventoryWorkflowInputDTO["items"] = [] + const variants: ConfirmVariantInventoryWorkflowInputDTO["variants"] = [] + + for (const orderItem of orderItems.items ?? []) { + items.push({ + variant_id: orderItem.variant.id, + quantity: orderItem.quantity, + id: orderItem.id, + }) + variants.push(orderItem.variant) + } + + return { + variants, + items, + } + }) + + const formatedInventoryItems = transform( + { + input: { + sales_channel_id: (orderItems as any).sales_channel_id, + variants, + items, + }, + }, + prepareConfirmInventoryInput + ) + + reserveInventoryStep(formatedInventoryItems) + const updatedOrder = convertDraftOrderStep({ id: input.id }) parallelize(