fix(inventory): delete reservation item inventory level required quantity (#13633)

Fixes #13625


---

> [!NOTE]
> Ensure `reserved_quantity` is adjusted when deleting reservation items via `deleteReservationItems`, with new integration tests covering deletion and inventory updates.
> 
> - **Inventory Module Service (`packages/modules/inventory/src/services/inventory-module.ts`)**:
>   - Add `deleteReservationItems` API (and transactional `_` variant) that hard-deletes reservations, then updates related inventory levels via `adjustInventoryLevelsForReservationsDeletion`.
>   - Wire through event emission/manager decorators consistent with existing patterns.
> - **Integration Tests (`packages/modules/inventory/integration-tests/__tests__/inventory-module-service.spec.ts`)**:
>   - Add tests for `deleteReservationItems` to verify deletion by id and `reserved_quantity` adjustments on inventory levels.
>   - Minor import update to include `ReservationItemDTO`.
> - **Changeset**:
>   - Patch release note for `@medusajs/inventory` documenting the fix.
> 
> <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ac6641a9ec9543115504407f708f81bd427c3444. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup>
This commit is contained in:
Nicolas Gorga
2025-10-02 09:59:13 -03:00
committed by GitHub
parent 9c957e1da0
commit ca334b7cc1
3 changed files with 131 additions and 1 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/inventory": patch
---
fix(inventory): Adjust inventory level required_quantity when reservation item is deleted through deleteReservationItems from module service

View File

@@ -1,4 +1,4 @@
import { IInventoryService, InventoryItemDTO } from "@medusajs/framework/types" import { IInventoryService, InventoryItemDTO, ReservationItemDTO } from "@medusajs/framework/types"
import { import {
BigNumber, BigNumber,
CommonEvents, CommonEvents,
@@ -552,6 +552,105 @@ moduleIntegrationTestRunner<IInventoryService>({
}) })
}) })
describe("deleteReservationItems", () => {
let inventoryItem: InventoryItemDTO
let reservationItems: ReservationItemDTO[]
beforeEach(async () => {
inventoryItem = await service.createInventoryItems({
sku: "test-sku",
origin_country: "test-country",
})
await service.createInventoryLevels([
{
inventory_item_id: inventoryItem.id,
location_id: "location-1",
stocked_quantity: 2,
},
{
inventory_item_id: inventoryItem.id,
location_id: "location-2",
stocked_quantity: 2,
},
])
reservationItems = await service.createReservationItems([
{
inventory_item_id: inventoryItem.id,
location_id: "location-1",
quantity: 2,
},
{
inventory_item_id: inventoryItem.id,
location_id: "location-2",
quantity: 2,
},
])
})
it("should delete reservation items by id", async () => {
const reservationItemsPreDeleted = await service.listReservationItems(
{
id: reservationItems.map((item) => item.id),
}
)
expect(reservationItemsPreDeleted).toEqual(
expect.arrayContaining([
expect.objectContaining({
inventory_item_id: inventoryItem.id,
location_id: "location-1",
quantity: 2,
}),
expect.objectContaining({
inventory_item_id: inventoryItem.id,
location_id: "location-2",
quantity: 2,
}),
])
)
expect(reservationItemsPreDeleted).toHaveLength(2)
await service.deleteReservationItems(
reservationItems.map((item) => item.id)
)
const reservationItemsPostDeleted =
await service.listReservationItems({
id: reservationItems.map((item) => item.id),
})
expect(reservationItemsPostDeleted).toEqual([])
})
it("should adjust inventory levels accordingly when removing reservations by id", async () => {
const reservationItem = (await service.listReservationItems({ location_id: "location-1"}))[0]
const inventoryLevelBefore =
await service.retrieveInventoryLevelByItemAndLocation(
inventoryItem.id,
"location-1"
)
expect(inventoryLevelBefore).toEqual(
expect.objectContaining({ reserved_quantity: 2 })
)
await service.deleteReservationItems(reservationItem.id)
const inventoryLevelAfter =
await service.retrieveInventoryLevelByItemAndLocation(
inventoryItem.id,
"location-1"
)
expect(inventoryLevelAfter).toEqual(
expect.objectContaining({ reserved_quantity: 0 })
)
})
})
describe("deleteReservationItemsByLineItem", () => { describe("deleteReservationItemsByLineItem", () => {
let inventoryItem: InventoryItemDTO let inventoryItem: InventoryItemDTO

View File

@@ -711,6 +711,32 @@ export default class InventoryModuleService
return result return result
} }
@InjectManager()
@EmitEvents()
// @ts-expect-error
async deleteReservationItems(
ids: string | string[],
@MedusaContext() context: Context = {}
): Promise<void> {
return await this.deleteReservationItems_(ids, context)
}
@InjectTransactionManager()
protected async deleteReservationItems_(
ids: string | string[],
@MedusaContext() context: Context = {}
): Promise<void> {
const reservations: InventoryTypes.ReservationItemDTO[] =
await this.reservationItemService_.list({ id: ids }, {}, context)
await super.deleteReservationItems(ids, context)
await this.adjustInventoryLevelsForReservationsDeletion(
reservations,
context
)
}
@InjectManager() @InjectManager()
@EmitEvents() @EmitEvents()
// @ts-expect-error // @ts-expect-error