diff --git a/.changeset/shy-camels-attend.md b/.changeset/shy-camels-attend.md new file mode 100644 index 0000000000..69b548953d --- /dev/null +++ b/.changeset/shy-camels-attend.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": minor +--- + +feat(medusa): remove reservations for all old line items when an order edit is accepted diff --git a/integration-tests/plugins/__tests__/inventory/order/order.js b/integration-tests/plugins/__tests__/inventory/order/order.js index 5f1bc676d7..cc15fbaea3 100644 --- a/integration-tests/plugins/__tests__/inventory/order/order.js +++ b/integration-tests/plugins/__tests__/inventory/order/order.js @@ -960,5 +960,107 @@ describe("/store/carts", () => { ) }) }) + + describe("POST /admin/order-edits", () => { + let order + + beforeEach(async () => { + const api = useApi() + + const cart = await simpleCartFactory(dbConnection, { + id: "test-cart", + sales_channel_id: "test-channel", + }) + + const cartId = cart.id + + await api.post( + `/store/carts/${cartId}/line-items`, + { + variant_id: variantId, + quantity: 3, + }, + { withCredentials: true } + ) + + await api.post( + `/store/carts/${cartId}`, + { + email: "test@test.com", + }, + { withCredentials: true } + ) + + await api.post(`/store/carts/${cartId}/payment-sessions`) + await api.post(`/store/carts/${cartId}/payment-session`, { + provider_id: "test-pay", + }) + const completeRes = await api.post(`/store/carts/${cartId}/complete`) + + expect(completeRes.status).toEqual(200) + expect(completeRes.data.type).toEqual("order") + + order = completeRes.data.data + }) + + it("deletes reservations when an order edit is confirmed", async () => { + const api = useApi() + + const lineItemIds = order.items.map((item) => item.id) + + const inventoryService = appContainer.resolve("inventoryService") + + const [, count] = await inventoryService.listReservationItems({ + line_item_id: lineItemIds, + }) + + expect(count).toEqual(1) + + let response = await api.post( + `/admin/order-edits/`, + { + order_id: order.id, + internal_note: "This is an internal note", + }, + adminHeaders + ) + + const orderEditId = response.data.order_edit.id + + const itemToUpdate = response.data.order_edit.items.find( + (item) => item.original_item_id === order.items[0].id + ) + + response = await api.post( + `/admin/order-edits/${orderEditId}/items/${itemToUpdate.id}`, + { quantity: 2 }, + adminHeaders + ) + + response = await api.post( + `/admin/order-edits/${orderEditId}/confirm`, + {}, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.order_edit).toEqual( + expect.objectContaining({ + id: orderEditId, + created_by: "admin_user", + confirmed_by: "admin_user", + confirmed_at: expect.any(String), + status: "confirmed", + }) + ) + + const [, countAfterConfirm] = + await inventoryService.listReservationItems({ + line_item_id: lineItemIds, + }) + + expect(countAfterConfirm).toEqual(0) + }) + }) }) }) diff --git a/integration-tests/plugins/factories/simple-cart-factory.ts b/integration-tests/plugins/factories/simple-cart-factory.ts index 664ef0d21e..e6c2d493f3 100644 --- a/integration-tests/plugins/factories/simple-cart-factory.ts +++ b/integration-tests/plugins/factories/simple-cart-factory.ts @@ -1,21 +1,21 @@ -import { Connection } from "typeorm" -import faker from "faker" -import { Cart } from "@medusajs/medusa" - -import { RegionFactoryData, simpleRegionFactory } from "./simple-region-factory" -import { - LineItemFactoryData, - simpleLineItemFactory, -} from "./simple-line-item-factory" import { AddressFactoryData, simpleAddressFactory, } from "./simple-address-factory" +import { Connection, DataSource } from "typeorm" +import { + LineItemFactoryData, + simpleLineItemFactory, +} from "./simple-line-item-factory" +import { RegionFactoryData, simpleRegionFactory } from "./simple-region-factory" import { ShippingMethodFactoryData, simpleShippingMethodFactory, } from "./simple-shipping-method-factory" +import { Cart } from "@medusajs/medusa" +import faker from "faker" + export type CartFactoryData = { id?: string region?: RegionFactoryData | string @@ -27,7 +27,7 @@ export type CartFactoryData = { } export const simpleCartFactory = async ( - connection: Connection, + dataSource: DataSource, data: CartFactoryData = {}, seed: number ): Promise => { @@ -35,16 +35,16 @@ export const simpleCartFactory = async ( faker.seed(seed) } - const manager = connection.manager + const manager = dataSource.manager let regionId: string if (typeof data.region === "string") { regionId = data.region } else { - const region = await simpleRegionFactory(connection, data.region) + const region = await simpleRegionFactory(dataSource, data.region) regionId = region.id } - const address = await simpleAddressFactory(connection, data.shipping_address) + const address = await simpleAddressFactory(dataSource, data.shipping_address) const id = data.id || `simple-cart-${Math.random() * 1000}` const toSave = manager.create(Cart, { @@ -60,12 +60,12 @@ export const simpleCartFactory = async ( const shippingMethods = data.shipping_methods || [] for (const sm of shippingMethods) { - await simpleShippingMethodFactory(connection, { ...sm, cart_id: id }) + await simpleShippingMethodFactory(dataSource, { ...sm, cart_id: id }) } const items = data.line_items - for (const item of items) { - await simpleLineItemFactory(connection, { ...item, cart_id: id }) + for (const item of items || []) { + await simpleLineItemFactory(dataSource, { ...item, cart_id: id }) } return cart diff --git a/packages/inventory/src/services/reservation-item.ts b/packages/inventory/src/services/reservation-item.ts index a2fb0b4a11..247eb24f1e 100644 --- a/packages/inventory/src/services/reservation-item.ts +++ b/packages/inventory/src/services/reservation-item.ts @@ -242,9 +242,10 @@ export default class ReservationItemService { context ) - const ops: Promise[] = [] + const ops: Promise[] = [ + itemRepository.softDelete({ line_item_id: lineItemId }) + ] for (const item of items) { - ops.push(itemRepository.softRemove({ line_item_id: lineItemId })) ops.push( this.inventoryLevelService_.adjustReservedQuantity( item.inventory_item_id, diff --git a/packages/medusa/src/services/__tests__/order-edit.ts b/packages/medusa/src/services/__tests__/order-edit.ts index d26e9b6150..7a0f3746a8 100644 --- a/packages/medusa/src/services/__tests__/order-edit.ts +++ b/packages/medusa/src/services/__tests__/order-edit.ts @@ -1,6 +1,4 @@ import { IdMap, MockManager, MockRepository } from "medusa-test-utils" -import { OrderEditItemChangeType, OrderEditStatus } from "../../models" -import EventBusService from "../event-bus" import { LineItemService, NewTotalsService, @@ -10,15 +8,18 @@ import { TaxProviderService, TotalsService, } from "../index" -import LineItemAdjustmentService from "../line-item-adjustment" +import { OrderEditItemChangeType, OrderEditStatus } from "../../models" + +import EventBusService from "../event-bus" import { EventBusServiceMock } from "../__mocks__/event-bus" -import { LineItemServiceMock } from "../__mocks__/line-item" +import LineItemAdjustmentService from "../line-item-adjustment" import { LineItemAdjustmentServiceMock } from "../__mocks__/line-item-adjustment" +import { LineItemServiceMock } from "../__mocks__/line-item" +import NewTotalsServiceMock from "../__mocks__/new-totals" import { OrderServiceMock } from "../__mocks__/order" +import { TotalsServiceMock } from "../__mocks__/totals" import { orderEditItemChangeServiceMock } from "../__mocks__/order-edit-item-change" import { taxProviderServiceMock } from "../__mocks__/tax-provider" -import { TotalsServiceMock } from "../__mocks__/totals" -import NewTotalsServiceMock from "../__mocks__/new-totals" const orderEditToUpdate = { id: IdMap.getId("order-edit-to-update"), diff --git a/packages/medusa/src/services/order-edit.ts b/packages/medusa/src/services/order-edit.ts index 8f9d426fe3..9391b19c88 100644 --- a/packages/medusa/src/services/order-edit.ts +++ b/packages/medusa/src/services/order-edit.ts @@ -1,13 +1,7 @@ -import { isDefined, MedusaError } from "medusa-core-utils" import { - DeepPartial, - EntityManager, - FindOptionsWhere, - ILike, - IsNull, -} from "typeorm" - -import { TransactionBaseService } from "../interfaces" + AddOrderEditLineItemInput, + CreateOrderEditInput, +} from "../types/order-edit" import { Cart, Order, @@ -15,14 +9,14 @@ import { OrderEditItemChangeType, OrderEditStatus, } from "../models" -import { OrderEditRepository } from "../repositories/order-edit" -import { FindConfig, Selector } from "../types/common" import { - AddOrderEditLineItemInput, - CreateOrderEditInput, -} from "../types/order-edit" -import { buildQuery, isString } from "../utils" -import EventBusService from "./event-bus" + DeepPartial, + EntityManager, + FindOptionsWhere, + ILike, + IsNull, +} from "typeorm" +import { FindConfig, Selector } from "../types/common" import { LineItemAdjustmentService, LineItemService, @@ -32,6 +26,13 @@ import { TaxProviderService, TotalsService, } from "./index" +import { MedusaError, isDefined } from "medusa-core-utils" +import { buildQuery, isString } from "../utils" + +import EventBusService from "./event-bus" +import { IInventoryService } from "@medusajs/types" +import { OrderEditRepository } from "../repositories/order-edit" +import { TransactionBaseService } from "../interfaces" type InjectedDependencies = { manager: EntityManager @@ -45,6 +46,8 @@ type InjectedDependencies = { taxProviderService: TaxProviderService lineItemAdjustmentService: LineItemAdjustmentService orderEditItemChangeService: OrderEditItemChangeService + + inventoryService?: IInventoryService } export default class OrderEditService extends TransactionBaseService { @@ -67,6 +70,7 @@ export default class OrderEditService extends TransactionBaseService { protected readonly taxProviderService_: TaxProviderService protected readonly lineItemAdjustmentService_: LineItemAdjustmentService protected readonly orderEditItemChangeService_: OrderEditItemChangeService + protected readonly inventoryService_: IInventoryService | undefined constructor({ orderEditRepository, @@ -78,6 +82,7 @@ export default class OrderEditService extends TransactionBaseService { orderEditItemChangeService, lineItemAdjustmentService, taxProviderService, + inventoryService, }: InjectedDependencies) { // eslint-disable-next-line prefer-rest-params super(arguments[0]) @@ -91,6 +96,7 @@ export default class OrderEditService extends TransactionBaseService { this.orderEditItemChangeService_ = orderEditItemChangeService this.lineItemAdjustmentService_ = lineItemAdjustmentService this.taxProviderService_ = taxProviderService + this.inventoryService_ = inventoryService } async retrieve( @@ -744,7 +750,7 @@ export default class OrderEditService extends TransactionBaseService { const lineItemServiceTx = this.lineItemService_.withTransaction(manager) - await Promise.all([ + const [lineItems] = await Promise.all([ lineItemServiceTx.update( { order_id: orderEdit.order_id }, { order_id: null } @@ -760,6 +766,17 @@ export default class OrderEditService extends TransactionBaseService { orderEdit = await orderEditRepository.save(orderEdit) + if (this.inventoryService_) { + await Promise.all( + lineItems.map( + async (lineItem) => + await this.inventoryService_!.deleteReservationItemsByLineItem( + lineItem.id + ) + ) + ) + } + await this.eventBusService_ .withTransaction(manager) .emit(OrderEditService.Events.CONFIRMED, { id: orderEditId })