chore(core-flows,inventory,types,medusa): add fixes to inventory module + location levels api (#7629)

what:

- santizes inputs to prevent reserved_quantity from being updated directly
- inventory items create api can create location levels
- add validation to update quantity of reservation items
- general cleanup

RESOLVES CORE-2254
This commit is contained in:
Riqwan Thamir
2024-06-06 14:58:17 +02:00
committed by GitHub
parent 3fbb8aa671
commit 0507dbe027
13 changed files with 366 additions and 93 deletions

View File

@@ -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", () => { describe("POST /admin/inventory-items/:id", () => {

View File

@@ -88,16 +88,25 @@ medusaIntegrationTestRunner({
it("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 = { const payload = {
quantity: 100000, quantity: 10000,
inventory_item_id: inventoryItem1.id, inventory_item_id: inventoryItem1.id,
line_item_id: "line-item-id", line_item_id: "line-item-id",
location_id: stockLocation1.id, location_id: stockLocation1.id,
} }
const res = await api const res = await api
.post(`/admin/reservations`, payload, adminHeaders) .post(
`/admin/reservations?fields=*inventory_item.location_levels`,
payload,
adminHeaders
)
.catch((err) => err) .catch((err) => err)
const res1 = await api.get(
`/admin/inventory-items/${inventoryItem1.id}`,
adminHeaders
)
expect(res.response.status).toBe(400) expect(res.response.status).toBe(400)
expect(res.response.data).toEqual({ expect(res.response.data).toEqual({
type: "not_allowed", type: "not_allowed",
@@ -133,8 +142,7 @@ medusaIntegrationTestRunner({
reservationId = reservationResponse.data.reservation.id reservationId = reservationResponse.data.reservation.id
}) })
// TODO: Implement this on the module it("should throw if available quantity is less than reservation quantity", async () => {
it.skip("should throw if available quantity is less than reservation quantity", async () => {
const payload = { quantity: 100000 } const payload = { quantity: 100000 }
const res = await api const res = await api
.post(`/admin/reservations/${reservationId}`, payload, adminHeaders) .post(`/admin/reservations/${reservationId}`, payload, adminHeaders)

View File

@@ -351,18 +351,33 @@ medusaIntegrationTestRunner({
}, },
]) ])
const inventoryItem = await inventoryModule.create({ const inventoryItem = (
sku: "inv-1234", 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, inventory_item_id: inventoryItem.id,
location_id: location.id, location_id: location.id,
stocked_quantity: 2, description: "test description",
reserved_quantity: 2, quantity: 2,
}, },
]) adminHeaders
)
const region = await regionModuleService.create({ const region = await regionModuleService.create({
name: "US", name: "US",

View File

@@ -3,23 +3,83 @@ import {
createWorkflow, createWorkflow,
transform, transform,
} from "@medusajs/workflows-sdk" } from "@medusajs/workflows-sdk"
import { import { createInventoryItemsStep } from "../steps"
attachInventoryItemToVariants,
createInventoryItemsStep,
validateInventoryItemsForCreate,
} from "../steps"
import { InventoryNext } from "@medusajs/types" import { InventoryNext } from "@medusajs/types"
import { createInventoryLevelsWorkflow } from "./create-inventory-levels"
type LocationLevelWithoutInventory = Omit<
InventoryNext.CreateInventoryLevelInput,
"inventory_item_id"
>
interface WorkflowInput { 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<number, LocationLevelWithoutInventory[]> = {}
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<number, LocationLevelWithoutInventory[]>
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 createInventoryItemsWorkflowId = "create-inventory-items-workflow"
export const createInventoryItemsWorkflow = createWorkflow( export const createInventoryItemsWorkflow = createWorkflow(
createInventoryItemsWorkflowId, createInventoryItemsWorkflowId,
(input: WorkflowData<WorkflowInput>) => { (input: WorkflowData<WorkflowInput>) => {
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 return items
} }

View File

@@ -145,6 +145,7 @@ export type ReservationItemDTO = {
line_item_id?: string | null line_item_id?: string | null
description?: string | null description?: string | null
created_by?: string | null created_by?: string | null
allow_backorder?: boolean
metadata: Record<string, unknown> | null metadata: Record<string, unknown> | null
created_at: string | Date created_at: string | Date
updated_at: string | Date updated_at: string | Date

View File

@@ -37,6 +37,11 @@ export interface ReservationItemDTO {
*/ */
description?: string | null 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. * The created by of the reservation item.
*/ */

View File

@@ -11,10 +11,6 @@ export interface CreateInventoryLevelInput {
* The stocked quantity of the associated inventory item in the associated location. * The stocked quantity of the associated inventory item in the associated location.
*/ */
stocked_quantity?: number 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. * The incoming quantity of the associated inventory item in the associated location.
*/ */

View File

@@ -17,6 +17,10 @@ export interface UpdateReservationItemInput {
* The description of the reservation item. * The description of the reservation item.
*/ */
description?: string 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. * Holds custom data in key-value pairs.
*/ */

View File

@@ -104,6 +104,7 @@ export const AdminCreateInventoryItem = z
requires_shipping: z.boolean().optional(), requires_shipping: z.boolean().optional(),
thumbnail: z.string().optional(), thumbnail: z.string().optional(),
metadata: z.record(z.string(), z.unknown()).optional(), metadata: z.record(z.string(), z.unknown()).optional(),
location_levels: z.array(AdminCreateInventoryLocationLevel).optional(),
}) })
.strict() .strict()

View File

@@ -209,6 +209,25 @@ moduleIntegrationTestRunner({
expect.objectContaining({ id: expect.any(String), ...data[1] }), 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", () => { describe("update", () => {
@@ -301,8 +320,10 @@ moduleIntegrationTestRunner({
describe("updateReservationItems", () => { describe("updateReservationItems", () => {
let reservationItem let reservationItem
let inventoryItem
beforeEach(async () => { beforeEach(async () => {
const inventoryItem = await service.create({ inventoryItem = await service.create({
sku: "test-sku", sku: "test-sku",
origin_country: "test-country", origin_country: "test-country",
}) })
@@ -311,26 +332,26 @@ moduleIntegrationTestRunner({
{ {
inventory_item_id: inventoryItem.id, inventory_item_id: inventoryItem.id,
location_id: "location-1", location_id: "location-1",
stocked_quantity: 2, stocked_quantity: 10,
}, },
{ {
inventory_item_id: inventoryItem.id, inventory_item_id: inventoryItem.id,
location_id: "location-2", location_id: "location-2",
stocked_quantity: 2, stocked_quantity: 10,
}, },
]) ])
reservationItem = await service.createReservationItems({ reservationItem = await service.createReservationItems({
inventory_item_id: inventoryItem.id, inventory_item_id: inventoryItem.id,
location_id: "location-1", location_id: "location-1",
quantity: 2, quantity: 3,
}) })
}) })
it("should update a reservationItem", async () => { it("should update a reservationItem", async () => {
const update = { const update = {
id: reservationItem.id, id: reservationItem.id,
quantity: 5, quantity: 1,
} }
const updated = await service.updateReservationItems(update) 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 () => { it("should adjust reserved_quantity of inventory level after updates increasing reserved quantity", async () => {
const update = { const update = {
id: reservationItem.id, id: reservationItem.id,
quantity: 5, quantity: 7,
} }
await service.updateReservationItems(update) await service.updateReservationItems(update)
@@ -358,7 +379,7 @@ moduleIntegrationTestRunner({
it("should adjust reserved_quantity of inventory level after updates decreasing reserved quantity", async () => { it("should adjust reserved_quantity of inventory level after updates decreasing reserved quantity", async () => {
const update = { const update = {
id: reservationItem.id, id: reservationItem.id,
quantity: 1, quantity: 2,
} }
await service.updateReservationItems(update) await service.updateReservationItems(update)
@@ -371,10 +392,26 @@ moduleIntegrationTestRunner({
expect(inventoryLevel.reserved_quantity).toEqual(update.quantity) 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", () => { describe("deleteReservationItemsByLineItem", () => {
let inventoryItem: InventoryItemDTO let inventoryItem: InventoryItemDTO
beforeEach(async () => { beforeEach(async () => {
inventoryItem = await service.create({ inventoryItem = await service.create({
sku: "test-sku", 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({ const reservationsPreDeleted = await service.listReservationItems({
line_item_id: "line-item-id", 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") await service.deleteReservationItemsByLineItem("line-item-id")
const inventoryLevel = const inventoryLevel =
@@ -543,7 +590,7 @@ moduleIntegrationTestRunner({
expect(reservationsPostDeleted).toEqual([]) 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 = const inventoryLevelPreDelete =
await service.retrieveInventoryLevelByItemAndLocation( await service.retrieveInventoryLevelByItemAndLocation(
inventoryItem.id, inventoryItem.id,
@@ -570,6 +617,7 @@ moduleIntegrationTestRunner({
describe("deleteInventoryItemLevelByLocationId", () => { describe("deleteInventoryItemLevelByLocationId", () => {
let inventoryItem: InventoryItemDTO let inventoryItem: InventoryItemDTO
beforeEach(async () => { beforeEach(async () => {
inventoryItem = await service.create({ inventoryItem = await service.create({
sku: "test-sku", sku: "test-sku",
@@ -581,7 +629,6 @@ moduleIntegrationTestRunner({
inventory_item_id: inventoryItem.id, inventory_item_id: inventoryItem.id,
location_id: "location-1", location_id: "location-1",
stocked_quantity: 2, stocked_quantity: 2,
reserved_quantity: 6,
}, },
{ {
inventory_item_id: inventoryItem.id, inventory_item_id: inventoryItem.id,
@@ -589,6 +636,12 @@ moduleIntegrationTestRunner({
stocked_quantity: 2, 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 () => { it("should remove inventory levels with given location id", async () => {
@@ -601,6 +654,7 @@ moduleIntegrationTestRunner({
expect.objectContaining({ expect.objectContaining({
stocked_quantity: 2, stocked_quantity: 2,
location_id: "location-1", location_id: "location-1",
reserved_quantity: 1,
}), }),
expect.objectContaining({ expect.objectContaining({
stocked_quantity: 2, stocked_quantity: 2,
@@ -741,6 +795,7 @@ moduleIntegrationTestRunner({
describe("retrieveAvailableQuantity", () => { describe("retrieveAvailableQuantity", () => {
let inventoryItem: InventoryItemDTO let inventoryItem: InventoryItemDTO
beforeEach(async () => { beforeEach(async () => {
inventoryItem = await service.create({ inventoryItem = await service.create({
sku: "test-sku", sku: "test-sku",
@@ -761,7 +816,6 @@ moduleIntegrationTestRunner({
inventory_item_id: inventoryItem.id, inventory_item_id: inventoryItem.id,
location_id: "location-2", location_id: "location-2",
stocked_quantity: 4, stocked_quantity: 4,
reserved_quantity: 2,
}, },
{ {
inventory_item_id: inventoryItem1.id, inventory_item_id: inventoryItem1.id,
@@ -774,6 +828,13 @@ moduleIntegrationTestRunner({
stocked_quantity: 3, 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 () => { it("should calculate current stocked quantity across locations", async () => {
@@ -781,17 +842,20 @@ moduleIntegrationTestRunner({
inventoryItem.id, inventoryItem.id,
["location-1", "location-2"] ["location-1", "location-2"]
) )
expect(level).toEqual(6) expect(level).toEqual(6)
}) })
}) })
describe("retrieveStockedQuantity", () => { describe("retrieveStockedQuantity", () => {
let inventoryItem: InventoryItemDTO let inventoryItem: InventoryItemDTO
beforeEach(async () => { beforeEach(async () => {
inventoryItem = await service.create({ inventoryItem = await service.create({
sku: "test-sku", sku: "test-sku",
origin_country: "test-country", origin_country: "test-country",
}) })
const inventoryItem1 = await service.create({ const inventoryItem1 = await service.create({
sku: "test-sku-1", sku: "test-sku-1",
origin_country: "test-country", origin_country: "test-country",
@@ -807,7 +871,6 @@ moduleIntegrationTestRunner({
inventory_item_id: inventoryItem.id, inventory_item_id: inventoryItem.id,
location_id: "location-2", location_id: "location-2",
stocked_quantity: 4, stocked_quantity: 4,
reserved_quantity: 2,
}, },
{ {
inventory_item_id: inventoryItem1.id, 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( const stockedQuantity = await service.retrieveStockedQuantity(
inventoryItem.id, inventoryItem.id,
["location-1", "location-2"] ["location-1", "location-2"]
@@ -831,13 +894,16 @@ moduleIntegrationTestRunner({
expect(stockedQuantity).toEqual(8) expect(stockedQuantity).toEqual(8)
}) })
}) })
describe("retrieveReservedQuantity", () => { describe("retrieveReservedQuantity", () => {
let inventoryItem: InventoryItemDTO let inventoryItem: InventoryItemDTO
beforeEach(async () => { beforeEach(async () => {
inventoryItem = await service.create({ inventoryItem = await service.create({
sku: "test-sku", sku: "test-sku",
origin_country: "test-country", origin_country: "test-country",
}) })
const inventoryItem1 = await service.create({ const inventoryItem1 = await service.create({
sku: "test-sku-1", sku: "test-sku-1",
origin_country: "test-country", origin_country: "test-country",
@@ -853,19 +919,34 @@ moduleIntegrationTestRunner({
inventory_item_id: inventoryItem.id, inventory_item_id: inventoryItem.id,
location_id: "location-2", location_id: "location-2",
stocked_quantity: 4, stocked_quantity: 4,
reserved_quantity: 2,
}, },
{ {
inventory_item_id: inventoryItem1.id, inventory_item_id: inventoryItem1.id,
location_id: "location-1", location_id: "location-1",
stocked_quantity: 3, stocked_quantity: 3,
reserved_quantity: 2,
}, },
{ {
inventory_item_id: inventoryItem.id, inventory_item_id: inventoryItem.id,
location_id: "location-3", location_id: "location-3",
stocked_quantity: 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", () => { describe("confirmInventory", () => {
let inventoryItem: InventoryItemDTO let inventoryItem: InventoryItemDTO
beforeEach(async () => { beforeEach(async () => {
inventoryItem = await service.create({ inventoryItem = await service.create({
sku: "test-sku", sku: "test-sku",
origin_country: "test-country", origin_country: "test-country",
}) })
await service.createInventoryLevels([ await service.createInventoryLevels({
{ inventory_item_id: inventoryItem.id,
inventory_item_id: inventoryItem.id, location_id: "location-1",
location_id: "location-1", stocked_quantity: 4,
stocked_quantity: 4, })
},
{ await service.createReservationItems({
inventory_item_id: inventoryItem.id, inventory_item_id: inventoryItem.id,
location_id: "location-2", location_id: "location-1",
stocked_quantity: 4, quantity: 2,
reserved_quantity: 2, })
},
])
}) })
it("should return true if quantity is less than or equal to available quantity", async () => { it("should return true if quantity is less than or equal to available quantity", async () => {
const reservedQuantity = await service.confirmInventory( const reservedQuantity = await service.confirmInventory(
inventoryItem.id, inventoryItem.id,
"location-1", ["location-1"],
2 2
) )
expect(reservedQuantity).toBeTruthy() 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( const reservedQuantity = await service.confirmInventory(
inventoryItem.id, inventoryItem.id,
"location-1", ["location-1"],
3 3
) )
expect(reservedQuantity).toBeTruthy() expect(reservedQuantity).toBeFalsy()
}) })
}) })
}) })

View File

@@ -1,5 +1,5 @@
import InventoryService from "./services/inventory"
import { ModuleExports } from "@medusajs/types" import { ModuleExports } from "@medusajs/types"
import InventoryService from "./services/inventory-module"
const service = InventoryService const service = InventoryService

View File

@@ -1,2 +1,2 @@
export { default as InventoryModuleService } from "./inventory"
export { default as InventoryLevelService } from "./inventory-level" export { default as InventoryLevelService } from "./inventory-level"
export { default as InventoryModuleService } from "./inventory-module"

View File

@@ -18,6 +18,7 @@ import {
MedusaContext, MedusaContext,
MedusaError, MedusaError,
ModulesSdkUtils, ModulesSdkUtils,
arrayDifference,
isDefined, isDefined,
isString, isString,
partitionArray, partitionArray,
@@ -151,6 +152,63 @@ export default class InventoryModuleService<
return inventoryLevels 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<TDTO = unknown>(
input: (TDTO & {
reserved_quantity?: number
})[]
): TDTO[] {
return input.map((input) => {
const { reserved_quantity, ...validInput } = input
return validInput as TDTO
})
}
private sanitizeInventoryItemInput<TDTO = unknown>(
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( async createReservationItems(
input: InventoryNext.CreateReservationItemInput[], input: InventoryNext.CreateReservationItemInput[],
context?: Context context?: Context
@@ -171,27 +229,12 @@ export default class InventoryModuleService<
InventoryNext.ReservationItemDTO[] | InventoryNext.ReservationItemDTO InventoryNext.ReservationItemDTO[] | InventoryNext.ReservationItemDTO
> { > {
const toCreate = Array.isArray(input) ? input : [input] 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) => { await this.ensureInventoryAvailability(sanitized, context)
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)
const created = await this.createReservationItems_(toCreate, context) const created = await this.createReservationItems_(toCreate, context)
@@ -289,8 +332,9 @@ export default class InventoryModuleService<
): Promise< ): Promise<
InventoryNext.InventoryItemDTO | InventoryNext.InventoryItemDTO[] 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) const result = await this.createInventoryItems_(toCreate, context)
context.messageAggregator?.saveRawMessageData( context.messageAggregator?.saveRawMessageData(
@@ -340,7 +384,9 @@ export default class InventoryModuleService<
): Promise< ): Promise<
InventoryNext.InventoryLevelDTO[] | InventoryNext.InventoryLevelDTO 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) const created = await this.createInventoryLevels_(toCreate, context)
@@ -398,7 +444,9 @@ export default class InventoryModuleService<
): Promise< ): Promise<
InventoryNext.InventoryItemDTO | InventoryNext.InventoryItemDTO[] 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) const result = await this.updateInventoryItems_(updates, context)
@@ -508,7 +556,9 @@ export default class InventoryModuleService<
): Promise< ): Promise<
InventoryNext.InventoryLevelDTO | InventoryNext.InventoryLevelDTO[] 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) const levels = await this.updateInventoryLevels_(input, context)
@@ -593,7 +643,6 @@ export default class InventoryModuleService<
InventoryNext.ReservationItemDTO | InventoryNext.ReservationItemDTO[] InventoryNext.ReservationItemDTO | InventoryNext.ReservationItemDTO[]
> { > {
const update = Array.isArray(input) ? input : [input] const update = Array.isArray(input) ? input : [input]
const result = await this.updateReservationItems_(update, context) const result = await this.updateReservationItems_(update, context)
context.messageAggregator?.saveRawMessageData( context.messageAggregator?.saveRawMessageData(
@@ -621,26 +670,47 @@ export default class InventoryModuleService<
input: (InventoryNext.UpdateReservationItemInput & { id: string })[], input: (InventoryNext.UpdateReservationItemInput & { id: string })[],
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
): Promise<TReservationItem[]> { ): Promise<TReservationItem[]> {
const ids = input.map((u) => u.id)
const reservationItems = await this.listReservationItems( const reservationItems = await this.listReservationItems(
{ id: input.map((u) => u.id) }, { id: ids },
{}, {},
context 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<string, ReservationItemDTO> = new Map( const reservationMap: Map<string, ReservationItemDTO> = new Map(
reservationItems.map((r) => [r.id, r]) 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<string, Map<string, number>> = input.reduce( const adjustments: Map<string, Map<string, number>> = input.reduce(
(acc, update) => { (acc, update) => {
const reservation = reservationMap.get(update.id) const reservation = reservationMap.get(update.id)!
if (!reservation) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Reservation item with id ${update.id} not found`
)
}
const locationMap = acc.get(reservation.inventory_item_id) ?? new Map() const locationMap = acc.get(reservation.inventory_item_id) ?? new Map()
if ( if (
@@ -676,6 +746,7 @@ export default class InventoryModuleService<
} }
acc.set(reservation.inventory_item_id, locationMap) acc.set(reservation.inventory_item_id, locationMap)
return acc return acc
}, },
new Map() new Map()