diff --git a/integration-tests/http/__tests__/inventory/admin/inventory.spec.ts b/integration-tests/http/__tests__/inventory/admin/inventory.spec.ts index 6043f6dded..25f5ba58af 100644 --- a/integration-tests/http/__tests__/inventory/admin/inventory.spec.ts +++ b/integration-tests/http/__tests__/inventory/admin/inventory.spec.ts @@ -612,6 +612,38 @@ medusaIntegrationTestRunner({ }) ) }) + + it("should create inventory items along with location levels", async () => { + const response = await api.post( + `/admin/inventory-items?fields=*location_levels`, + { + sku: "test-sku", + location_levels: [ + { + location_id: stockLocation1.id, + stocked_quantity: 20, + incoming_quantity: 40, + }, + ], + }, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.inventory_item).toEqual( + expect.objectContaining({ + sku: "test-sku", + location_levels: [ + expect.objectContaining({ + id: expect.any(String), + inventory_item_id: expect.any(String), + stocked_quantity: 20, + incoming_quantity: 40, + }), + ], + }) + ) + }) }) describe("POST /admin/inventory-items/:id", () => { diff --git a/integration-tests/http/__tests__/reservations/admin/reservations.spec.ts b/integration-tests/http/__tests__/reservations/admin/reservations.spec.ts index 1f026d6acf..9fbc193b8a 100644 --- a/integration-tests/http/__tests__/reservations/admin/reservations.spec.ts +++ b/integration-tests/http/__tests__/reservations/admin/reservations.spec.ts @@ -88,16 +88,25 @@ medusaIntegrationTestRunner({ it("should throw if available quantity is less than reservation quantity", async () => { const payload = { - quantity: 100000, + quantity: 10000, inventory_item_id: inventoryItem1.id, line_item_id: "line-item-id", location_id: stockLocation1.id, } const res = await api - .post(`/admin/reservations`, payload, adminHeaders) + .post( + `/admin/reservations?fields=*inventory_item.location_levels`, + payload, + adminHeaders + ) .catch((err) => err) + const res1 = await api.get( + `/admin/inventory-items/${inventoryItem1.id}`, + adminHeaders + ) + expect(res.response.status).toBe(400) expect(res.response.data).toEqual({ type: "not_allowed", @@ -133,8 +142,7 @@ medusaIntegrationTestRunner({ reservationId = reservationResponse.data.reservation.id }) - // TODO: Implement this on the module - it.skip("should throw if available quantity is less than reservation quantity", async () => { + it("should throw if available quantity is less than reservation quantity", async () => { const payload = { quantity: 100000 } const res = await api .post(`/admin/reservations/${reservationId}`, payload, adminHeaders) diff --git a/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts b/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts index d6e202cbc9..6e01f48116 100644 --- a/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts +++ b/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts @@ -351,18 +351,33 @@ medusaIntegrationTestRunner({ }, ]) - const inventoryItem = await inventoryModule.create({ - sku: "inv-1234", - }) + const inventoryItem = ( + await api.post( + `/admin/inventory-items`, + { + sku: "inv-1234", + location_levels: [ + { + location_id: location.id, + stocked_quantity: 2, + }, + ], + }, + adminHeaders + ) + ).data.inventory_item - await inventoryModule.createInventoryLevels([ + await api.post( + `/admin/reservations`, { + line_item_id: "line-item-id-1", inventory_item_id: inventoryItem.id, location_id: location.id, - stocked_quantity: 2, - reserved_quantity: 2, + description: "test description", + quantity: 2, }, - ]) + adminHeaders + ) const region = await regionModuleService.create({ name: "US", diff --git a/packages/core/core-flows/src/inventory/workflows/create-inventory-items.ts b/packages/core/core-flows/src/inventory/workflows/create-inventory-items.ts index f077e353aa..b889083946 100644 --- a/packages/core/core-flows/src/inventory/workflows/create-inventory-items.ts +++ b/packages/core/core-flows/src/inventory/workflows/create-inventory-items.ts @@ -3,23 +3,83 @@ import { createWorkflow, transform, } from "@medusajs/workflows-sdk" -import { - attachInventoryItemToVariants, - createInventoryItemsStep, - validateInventoryItemsForCreate, -} from "../steps" +import { createInventoryItemsStep } from "../steps" import { InventoryNext } from "@medusajs/types" +import { createInventoryLevelsWorkflow } from "./create-inventory-levels" +type LocationLevelWithoutInventory = Omit< + InventoryNext.CreateInventoryLevelInput, + "inventory_item_id" +> interface WorkflowInput { - items: InventoryNext.CreateInventoryItemInput[] + items: (InventoryNext.CreateInventoryItemInput & { + location_levels?: LocationLevelWithoutInventory[] + })[] +} + +const buildLocationLevelMapAndItemData = (data: WorkflowInput) => { + data.items = data.items ?? [] + const inventoryItems: InventoryNext.CreateInventoryItemInput[] = [] + // Keep an index to location levels mapping to inject the created inventory item + // id into the location levels workflow input + const locationLevelMap: Record = {} + + data.items.forEach(({ location_levels, ...inventoryItem }, index) => { + locationLevelMap[index] = location_levels?.length ? location_levels : [] + + inventoryItems.push(inventoryItem) + }) + + return { + locationLevelMap, + inventoryItems, + } +} + +const buildInventoryLevelsInput = (data: { + locationLevelMap: Record + items: InventoryNext.InventoryItemDTO[] +}) => { + const inventoryLevels: InventoryNext.CreateInventoryLevelInput[] = [] + let index = 0 + + // The order of the input is critical to accurately create location levels for + // the right inventory item + data.items.forEach((item, index) => { + const locationLevels = data.locationLevelMap[index] || [] + + locationLevels.forEach((locationLevel) => + inventoryLevels.push({ + ...locationLevel, + inventory_item_id: item.id, + }) + ) + }) + + return { + input: { + inventory_levels: inventoryLevels, + }, + } } export const createInventoryItemsWorkflowId = "create-inventory-items-workflow" export const createInventoryItemsWorkflow = createWorkflow( createInventoryItemsWorkflowId, (input: WorkflowData) => { - const items = createInventoryItemsStep(input.items) + const { locationLevelMap, inventoryItems } = transform( + input, + buildLocationLevelMapAndItemData + ) + const items = createInventoryItemsStep(inventoryItems) + + const inventoryLevelsInput = transform( + { items, locationLevelMap }, + buildInventoryLevelsInput + ) + + createInventoryLevelsWorkflow.runAsStep(inventoryLevelsInput) return items } diff --git a/packages/core/types/src/inventory/common.ts b/packages/core/types/src/inventory/common.ts index a7d5f846a7..c17b3365b7 100644 --- a/packages/core/types/src/inventory/common.ts +++ b/packages/core/types/src/inventory/common.ts @@ -145,6 +145,7 @@ export type ReservationItemDTO = { line_item_id?: string | null description?: string | null created_by?: string | null + allow_backorder?: boolean metadata: Record | null created_at: string | Date updated_at: string | Date diff --git a/packages/core/types/src/inventory/common/reservation-item.ts b/packages/core/types/src/inventory/common/reservation-item.ts index 20c771b2b2..1a6c8a6397 100644 --- a/packages/core/types/src/inventory/common/reservation-item.ts +++ b/packages/core/types/src/inventory/common/reservation-item.ts @@ -37,6 +37,11 @@ export interface ReservationItemDTO { */ description?: string | null + /** + * Allow backorder of the item. If true, it won't check inventory levels before reserving it. + */ + allow_backorder?: boolean + /** * The created by of the reservation item. */ diff --git a/packages/core/types/src/inventory/mutations/inventory-level.ts b/packages/core/types/src/inventory/mutations/inventory-level.ts index 22f9dcbf7b..088c494ed7 100644 --- a/packages/core/types/src/inventory/mutations/inventory-level.ts +++ b/packages/core/types/src/inventory/mutations/inventory-level.ts @@ -11,10 +11,6 @@ export interface CreateInventoryLevelInput { * The stocked quantity of the associated inventory item in the associated location. */ stocked_quantity?: number - /** - * The reserved quantity of the associated inventory item in the associated location. - */ - reserved_quantity?: number /** * The incoming quantity of the associated inventory item in the associated location. */ diff --git a/packages/core/types/src/inventory/mutations/reservation-item.ts b/packages/core/types/src/inventory/mutations/reservation-item.ts index 85be2fedcd..bead659fa2 100644 --- a/packages/core/types/src/inventory/mutations/reservation-item.ts +++ b/packages/core/types/src/inventory/mutations/reservation-item.ts @@ -17,6 +17,10 @@ export interface UpdateReservationItemInput { * The description of the reservation item. */ description?: string + /** + * Allow backorder of the item. If true, it won't check inventory levels before reserving it. + */ + allow_backorder?: boolean /** * Holds custom data in key-value pairs. */ diff --git a/packages/medusa/src/api/admin/inventory-items/validators.ts b/packages/medusa/src/api/admin/inventory-items/validators.ts index 7f11888834..39f7cea57b 100644 --- a/packages/medusa/src/api/admin/inventory-items/validators.ts +++ b/packages/medusa/src/api/admin/inventory-items/validators.ts @@ -104,6 +104,7 @@ export const AdminCreateInventoryItem = z requires_shipping: z.boolean().optional(), thumbnail: z.string().optional(), metadata: z.record(z.string(), z.unknown()).optional(), + location_levels: z.array(AdminCreateInventoryLocationLevel).optional(), }) .strict() 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 3e4cbb14dd..6d07dbed03 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 @@ -209,6 +209,25 @@ moduleIntegrationTestRunner({ expect.objectContaining({ id: expect.any(String), ...data[1] }), ]) }) + + it("should not create with required quantity", async () => { + const data = { + inventory_item_id: inventoryItem.id, + location_id: "location-1", + stocked_quantity: 2, + reserved_quantity: 1000, + } + + const inventoryLevel = await service.createInventoryLevels(data) + + expect(inventoryLevel).toEqual( + expect.objectContaining({ + id: expect.any(String), + ...data, + reserved_quantity: 0, + }) + ) + }) }) describe("update", () => { @@ -301,8 +320,10 @@ moduleIntegrationTestRunner({ describe("updateReservationItems", () => { let reservationItem + let inventoryItem + beforeEach(async () => { - const inventoryItem = await service.create({ + inventoryItem = await service.create({ sku: "test-sku", origin_country: "test-country", }) @@ -311,26 +332,26 @@ moduleIntegrationTestRunner({ { inventory_item_id: inventoryItem.id, location_id: "location-1", - stocked_quantity: 2, + stocked_quantity: 10, }, { inventory_item_id: inventoryItem.id, location_id: "location-2", - stocked_quantity: 2, + stocked_quantity: 10, }, ]) reservationItem = await service.createReservationItems({ inventory_item_id: inventoryItem.id, location_id: "location-1", - quantity: 2, + quantity: 3, }) }) it("should update a reservationItem", async () => { const update = { id: reservationItem.id, - quantity: 5, + quantity: 1, } const updated = await service.updateReservationItems(update) @@ -341,7 +362,7 @@ moduleIntegrationTestRunner({ it("should adjust reserved_quantity of inventory level after updates increasing reserved quantity", async () => { const update = { id: reservationItem.id, - quantity: 5, + quantity: 7, } await service.updateReservationItems(update) @@ -358,7 +379,7 @@ moduleIntegrationTestRunner({ it("should adjust reserved_quantity of inventory level after updates decreasing reserved quantity", async () => { const update = { id: reservationItem.id, - quantity: 1, + quantity: 2, } await service.updateReservationItems(update) @@ -371,10 +392,26 @@ moduleIntegrationTestRunner({ expect(inventoryLevel.reserved_quantity).toEqual(update.quantity) }) + + it("should throw error when increasing reserved quantity beyond availability", async () => { + const update = { + id: reservationItem.id, + quantity: 10, + } + + const error = await service + .updateReservationItems(update) + .catch((e) => e) + + expect(error.message).toEqual( + `Not enough stock available for item ${inventoryItem.id} at location location-1` + ) + }) }) describe("deleteReservationItemsByLineItem", () => { let inventoryItem: InventoryItemDTO + beforeEach(async () => { inventoryItem = await service.create({ sku: "test-sku", @@ -415,7 +452,7 @@ moduleIntegrationTestRunner({ ]) }) - it("deleted reseravation items by line item and restore them", async () => { + it("should delete reseravation items by line item and restore them", async () => { const reservationsPreDeleted = await service.listReservationItems({ line_item_id: "line-item-id", }) @@ -461,7 +498,17 @@ moduleIntegrationTestRunner({ ]) }) - it("adjusts inventory levels accordingly when removing reservations by line item", async () => { + it("should adjust inventory levels accordingly when removing reservations by line item", async () => { + const inventoryLevelBefore = + await service.retrieveInventoryLevelByItemAndLocation( + inventoryItem.id, + "location-1" + ) + + expect(inventoryLevelBefore).toEqual( + expect.objectContaining({ reserved_quantity: 6 }) + ) + await service.deleteReservationItemsByLineItem("line-item-id") const inventoryLevel = @@ -543,7 +590,7 @@ moduleIntegrationTestRunner({ expect(reservationsPostDeleted).toEqual([]) }) - it("adjusts inventory levels accordingly when removing reservations by line item", async () => { + it("should adjust inventory levels accordingly when removing reservations by line item", async () => { const inventoryLevelPreDelete = await service.retrieveInventoryLevelByItemAndLocation( inventoryItem.id, @@ -570,6 +617,7 @@ moduleIntegrationTestRunner({ describe("deleteInventoryItemLevelByLocationId", () => { let inventoryItem: InventoryItemDTO + beforeEach(async () => { inventoryItem = await service.create({ sku: "test-sku", @@ -581,7 +629,6 @@ moduleIntegrationTestRunner({ inventory_item_id: inventoryItem.id, location_id: "location-1", stocked_quantity: 2, - reserved_quantity: 6, }, { inventory_item_id: inventoryItem.id, @@ -589,6 +636,12 @@ moduleIntegrationTestRunner({ stocked_quantity: 2, }, ]) + + await service.createReservationItems({ + inventory_item_id: inventoryItem.id, + location_id: "location-1", + quantity: 1, + }) }) it("should remove inventory levels with given location id", async () => { @@ -601,6 +654,7 @@ moduleIntegrationTestRunner({ expect.objectContaining({ stocked_quantity: 2, location_id: "location-1", + reserved_quantity: 1, }), expect.objectContaining({ stocked_quantity: 2, @@ -741,6 +795,7 @@ moduleIntegrationTestRunner({ describe("retrieveAvailableQuantity", () => { let inventoryItem: InventoryItemDTO + beforeEach(async () => { inventoryItem = await service.create({ sku: "test-sku", @@ -761,7 +816,6 @@ moduleIntegrationTestRunner({ inventory_item_id: inventoryItem.id, location_id: "location-2", stocked_quantity: 4, - reserved_quantity: 2, }, { inventory_item_id: inventoryItem1.id, @@ -774,6 +828,13 @@ moduleIntegrationTestRunner({ stocked_quantity: 3, }, ]) + + await service.createReservationItems({ + line_item_id: "test", + inventory_item_id: inventoryItem.id, + location_id: "location-2", + quantity: 2, + }) }) it("should calculate current stocked quantity across locations", async () => { @@ -781,17 +842,20 @@ moduleIntegrationTestRunner({ inventoryItem.id, ["location-1", "location-2"] ) + expect(level).toEqual(6) }) }) describe("retrieveStockedQuantity", () => { let inventoryItem: InventoryItemDTO + beforeEach(async () => { inventoryItem = await service.create({ sku: "test-sku", origin_country: "test-country", }) + const inventoryItem1 = await service.create({ sku: "test-sku-1", origin_country: "test-country", @@ -807,7 +871,6 @@ moduleIntegrationTestRunner({ inventory_item_id: inventoryItem.id, location_id: "location-2", stocked_quantity: 4, - reserved_quantity: 2, }, { inventory_item_id: inventoryItem1.id, @@ -822,7 +885,7 @@ moduleIntegrationTestRunner({ ]) }) - it("retrieves stocked location", async () => { + it("should retrieve stocked location", async () => { const stockedQuantity = await service.retrieveStockedQuantity( inventoryItem.id, ["location-1", "location-2"] @@ -831,13 +894,16 @@ moduleIntegrationTestRunner({ expect(stockedQuantity).toEqual(8) }) }) + describe("retrieveReservedQuantity", () => { let inventoryItem: InventoryItemDTO + beforeEach(async () => { inventoryItem = await service.create({ sku: "test-sku", origin_country: "test-country", }) + const inventoryItem1 = await service.create({ sku: "test-sku-1", origin_country: "test-country", @@ -853,19 +919,34 @@ moduleIntegrationTestRunner({ inventory_item_id: inventoryItem.id, location_id: "location-2", stocked_quantity: 4, - reserved_quantity: 2, }, { inventory_item_id: inventoryItem1.id, location_id: "location-1", stocked_quantity: 3, - reserved_quantity: 2, }, { inventory_item_id: inventoryItem.id, location_id: "location-3", stocked_quantity: 3, - reserved_quantity: 2, + }, + ]) + + await service.createReservationItems([ + { + inventory_item_id: inventoryItem.id, + location_id: "location-2", + quantity: 2, + }, + { + inventory_item_id: inventoryItem1.id, + location_id: "location-1", + quantity: 2, + }, + { + inventory_item_id: inventoryItem.id, + location_id: "location-3", + quantity: 2, }, ]) }) @@ -882,45 +963,44 @@ moduleIntegrationTestRunner({ describe("confirmInventory", () => { let inventoryItem: InventoryItemDTO + beforeEach(async () => { inventoryItem = await service.create({ sku: "test-sku", origin_country: "test-country", }) - await service.createInventoryLevels([ - { - inventory_item_id: inventoryItem.id, - location_id: "location-1", - stocked_quantity: 4, - }, - { - inventory_item_id: inventoryItem.id, - location_id: "location-2", - stocked_quantity: 4, - reserved_quantity: 2, - }, - ]) + await service.createInventoryLevels({ + inventory_item_id: inventoryItem.id, + location_id: "location-1", + stocked_quantity: 4, + }) + + await service.createReservationItems({ + inventory_item_id: inventoryItem.id, + location_id: "location-1", + quantity: 2, + }) }) it("should return true if quantity is less than or equal to available quantity", async () => { const reservedQuantity = await service.confirmInventory( inventoryItem.id, - "location-1", + ["location-1"], 2 ) expect(reservedQuantity).toBeTruthy() }) - it("should return true if quantity is more than available quantity", async () => { + it("should return false if quantity is more than available quantity", async () => { const reservedQuantity = await service.confirmInventory( inventoryItem.id, - "location-1", + ["location-1"], 3 ) - expect(reservedQuantity).toBeTruthy() + expect(reservedQuantity).toBeFalsy() }) }) }) diff --git a/packages/modules/inventory-next/src/module-definition.ts b/packages/modules/inventory-next/src/module-definition.ts index 81e4aefe71..c55dc2e8ce 100644 --- a/packages/modules/inventory-next/src/module-definition.ts +++ b/packages/modules/inventory-next/src/module-definition.ts @@ -1,5 +1,5 @@ -import InventoryService from "./services/inventory" import { ModuleExports } from "@medusajs/types" +import InventoryService from "./services/inventory-module" const service = InventoryService diff --git a/packages/modules/inventory-next/src/services/index.ts b/packages/modules/inventory-next/src/services/index.ts index 3cf8942bef..5fb5e46abf 100644 --- a/packages/modules/inventory-next/src/services/index.ts +++ b/packages/modules/inventory-next/src/services/index.ts @@ -1,2 +1,2 @@ -export { default as InventoryModuleService } from "./inventory" export { default as InventoryLevelService } from "./inventory-level" +export { default as InventoryModuleService } from "./inventory-module" diff --git a/packages/modules/inventory-next/src/services/inventory.ts b/packages/modules/inventory-next/src/services/inventory-module.ts similarity index 92% rename from packages/modules/inventory-next/src/services/inventory.ts rename to packages/modules/inventory-next/src/services/inventory-module.ts index 0c7593a510..c76325257d 100644 --- a/packages/modules/inventory-next/src/services/inventory.ts +++ b/packages/modules/inventory-next/src/services/inventory-module.ts @@ -18,6 +18,7 @@ import { MedusaContext, MedusaError, ModulesSdkUtils, + arrayDifference, isDefined, isString, partitionArray, @@ -151,6 +152,63 @@ export default class InventoryModuleService< return inventoryLevels } + // reserved_quantity should solely be handled through creating & updating reservation items + // We sanitize the inputs here to prevent that from being used to update it + private sanitizeInventoryLevelInput( + input: (TDTO & { + reserved_quantity?: number + })[] + ): TDTO[] { + return input.map((input) => { + const { reserved_quantity, ...validInput } = input + + return validInput as TDTO + }) + } + + private sanitizeInventoryItemInput( + input: (TDTO & { + location_levels?: object[] + })[] + ): TDTO[] { + return input.map((input) => { + const { location_levels, ...validInput } = input + + return validInput as TDTO + }) + } + + private async ensureInventoryAvailability( + data: { + allow_backorder: boolean + inventory_item_id: string + location_id: string + quantity: number + }[], + context: Context + ) { + const checkLevels = data.map(async (reservation) => { + if (!!reservation.allow_backorder) { + return + } + + const available = await this.retrieveAvailableQuantity( + reservation.inventory_item_id, + [reservation.location_id], + context + ) + + if (available < reservation.quantity) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + `Not enough stock available for item ${reservation.inventory_item_id} at location ${reservation.location_id}` + ) + } + }) + + await promiseAll(checkLevels) + } + async createReservationItems( input: InventoryNext.CreateReservationItemInput[], context?: Context @@ -171,27 +229,12 @@ export default class InventoryModuleService< InventoryNext.ReservationItemDTO[] | InventoryNext.ReservationItemDTO > { const toCreate = Array.isArray(input) ? input : [input] + const sanitized = toCreate.map((d) => ({ + ...d, + allow_backorder: d.allow_backorder || false, + })) - const checkLevels = toCreate.map(async (item) => { - if (!!item.allow_backorder) { - return - } - - const available = await this.retrieveAvailableQuantity( - item.inventory_item_id, - [item.location_id], - context - ) - - if (available < item.quantity) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - `Not enough stock available for item ${item.inventory_item_id} at location ${item.location_id}` - ) - } - }) - - await promiseAll(checkLevels) + await this.ensureInventoryAvailability(sanitized, context) const created = await this.createReservationItems_(toCreate, context) @@ -289,8 +332,9 @@ export default class InventoryModuleService< ): Promise< InventoryNext.InventoryItemDTO | InventoryNext.InventoryItemDTO[] > { - const toCreate = Array.isArray(input) ? input : [input] - + const toCreate = this.sanitizeInventoryItemInput( + Array.isArray(input) ? input : [input] + ) const result = await this.createInventoryItems_(toCreate, context) context.messageAggregator?.saveRawMessageData( @@ -340,7 +384,9 @@ export default class InventoryModuleService< ): Promise< InventoryNext.InventoryLevelDTO[] | InventoryNext.InventoryLevelDTO > { - const toCreate = Array.isArray(input) ? input : [input] + const toCreate = this.sanitizeInventoryLevelInput( + Array.isArray(input) ? input : [input] + ) const created = await this.createInventoryLevels_(toCreate, context) @@ -398,7 +444,9 @@ export default class InventoryModuleService< ): Promise< InventoryNext.InventoryItemDTO | InventoryNext.InventoryItemDTO[] > { - const updates = Array.isArray(input) ? input : [input] + const updates = this.sanitizeInventoryItemInput( + Array.isArray(input) ? input : [input] + ) const result = await this.updateInventoryItems_(updates, context) @@ -508,7 +556,9 @@ export default class InventoryModuleService< ): Promise< InventoryNext.InventoryLevelDTO | InventoryNext.InventoryLevelDTO[] > { - const input = Array.isArray(updates) ? updates : [updates] + const input = this.sanitizeInventoryLevelInput( + Array.isArray(updates) ? updates : [updates] + ) const levels = await this.updateInventoryLevels_(input, context) @@ -593,7 +643,6 @@ export default class InventoryModuleService< InventoryNext.ReservationItemDTO | InventoryNext.ReservationItemDTO[] > { const update = Array.isArray(input) ? input : [input] - const result = await this.updateReservationItems_(update, context) context.messageAggregator?.saveRawMessageData( @@ -621,26 +670,47 @@ export default class InventoryModuleService< input: (InventoryNext.UpdateReservationItemInput & { id: string })[], @MedusaContext() context: Context = {} ): Promise { + const ids = input.map((u) => u.id) const reservationItems = await this.listReservationItems( - { id: input.map((u) => u.id) }, + { id: ids }, {}, context ) + const diff = arrayDifference( + ids, + reservationItems.map((i) => i.id) + ) + + if (diff.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Reservation item with id ${diff.join(", ")} not found` + ) + } + const reservationMap: Map = new Map( reservationItems.map((r) => [r.id, r]) ) + const availabilityData = input.map((data) => { + const reservation = reservationMap.get(data.id)! + + return { + ...data, + quantity: data.quantity ?? reservation.quantity, + allow_backorder: + data.allow_backorder || reservation.allow_backorder || false, + inventory_item_id: reservation.inventory_item_id, + location_id: data.location_id ?? reservation.location_id, + } + }) + + await this.ensureInventoryAvailability(availabilityData, context) + const adjustments: Map> = input.reduce( (acc, update) => { - const reservation = reservationMap.get(update.id) - if (!reservation) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Reservation item with id ${update.id} not found` - ) - } - + const reservation = reservationMap.get(update.id)! const locationMap = acc.get(reservation.inventory_item_id) ?? new Map() if ( @@ -676,6 +746,7 @@ export default class InventoryModuleService< } acc.set(reservation.inventory_item_id, locationMap) + return acc }, new Map()