Files
medusa-store/packages/modules/inventory/src/services/inventory-module.ts
Adrien de Peretti d828005354 chore(): improve inventory module (#13463)
RESOVLES CORE-1185

**What**
Reduce higher function call to reduce overserialization
2025-09-11 10:11:49 +00:00

1204 lines
33 KiB
TypeScript

import {
BigNumberInput,
Context,
DAL,
IInventoryService,
InferEntityType,
InternalModuleDeclaration,
InventoryTypes,
ModuleJoinerConfig,
ModulesSdkTypes,
ReservationItemDTO,
RestoreReturn,
SoftDeleteReturn,
} from "@medusajs/framework/types"
import {
arrayDifference,
BigNumber,
EmitEvents,
InjectManager,
InjectTransactionManager,
isDefined,
isString,
MathBN,
MedusaContext,
MedusaError,
MedusaService,
partitionArray,
} from "@medusajs/framework/utils"
import { InventoryItem, InventoryLevel, ReservationItem } from "@models"
import { joinerConfig } from "../joiner-config"
import { applyEntityHooks } from "../utils/apply-decorators"
import InventoryLevelService from "./inventory-level"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
inventoryItemService: ModulesSdkTypes.IMedusaInternalService<any>
inventoryLevelService: InventoryLevelService
reservationItemService: ModulesSdkTypes.IMedusaInternalService<any>
}
type InventoryItemCheckLevel = {
id?: string
location_id: string
inventory_item_id: string
quantity?: BigNumberInput
allow_backorder?: boolean
}
applyEntityHooks()
export default class InventoryModuleService
extends MedusaService<{
InventoryItem: {
dto: InventoryTypes.InventoryItemDTO
}
InventoryLevel: {
dto: InventoryTypes.InventoryLevelDTO
}
ReservationItem: {
dto: InventoryTypes.ReservationItemDTO
}
}>({
InventoryItem,
InventoryLevel,
ReservationItem,
})
implements IInventoryService
{
protected baseRepository_: DAL.RepositoryService
protected readonly inventoryItemService_: ModulesSdkTypes.IMedusaInternalService<
typeof InventoryItem
>
protected readonly reservationItemService_: ModulesSdkTypes.IMedusaInternalService<
typeof ReservationItem
>
protected readonly inventoryLevelService_: InventoryLevelService
constructor(
{
baseRepository,
inventoryItemService,
inventoryLevelService,
reservationItemService,
}: InjectedDependencies,
protected readonly moduleDeclaration?: InternalModuleDeclaration
) {
// @ts-ignore
// eslint-disable-next-line prefer-rest-params
super(...arguments)
this.baseRepository_ = baseRepository
this.inventoryItemService_ = inventoryItemService
this.inventoryLevelService_ = inventoryLevelService
this.reservationItemService_ = reservationItemService
}
__joinerConfig(): ModuleJoinerConfig {
return joinerConfig
}
private async ensureInventoryLevels(
data: InventoryItemCheckLevel[],
options?: {
validateQuantityAtLocation?: boolean
},
context?: Context
): Promise<InventoryTypes.InventoryLevelDTO[]> {
options ??= {}
const validateQuantityAtLocation =
options.validateQuantityAtLocation ?? false
const data_ = data.map((dt: any) => ({
location_id: dt.location_id,
inventory_item_id: dt.inventory_item_id,
})) as InventoryItemCheckLevel[]
const [idData, itemLocationData] = partitionArray(
data_,
({ id }) => !!id
) as [
{ id: string }[],
{ location_id: string; inventory_item_id: string }[]
]
const inventoryLevels = await this.inventoryLevelService_.list(
{
$or: [
{ id: idData.filter(({ id }) => !!id).map((e) => e.id) },
...itemLocationData,
],
},
{},
context
)
const inventoryLevelIdMap: Map<string, InventoryTypes.InventoryLevelDTO> =
new Map(inventoryLevels.map((level) => [level.id, level]))
const inventoryLevelItemLocationMap: Map<
string,
Map<string, InventoryTypes.InventoryLevelDTO>
> = inventoryLevels.reduce((acc, curr) => {
const inventoryLevelMap = acc.get(curr.inventory_item_id) ?? new Map()
inventoryLevelMap.set(curr.location_id, curr)
acc.set(curr.inventory_item_id, inventoryLevelMap)
return acc
}, new Map())
const missing = data.filter((item) => {
if (item.id) {
return !inventoryLevelIdMap.has(item.id!)
}
return !inventoryLevelItemLocationMap
.get(item.inventory_item_id)
?.has(item.location_id)
})
if (missing.length) {
const error = missing
.map((missing) => {
if ("id" in missing) {
return `Inventory level with id ${missing.id} does not exist`
}
return `Item ${missing.inventory_item_id} is not stocked at location ${missing.location_id}`
})
.join(", ")
throw new MedusaError(MedusaError.Types.NOT_FOUND, error)
}
if (validateQuantityAtLocation) {
for (const item of data) {
if (!!item.allow_backorder) {
continue
}
const locations = inventoryLevelItemLocationMap.get(
item.inventory_item_id
)!
const level = locations?.get(item.location_id)!
if (MathBN.lt(level.available_quantity, item.quantity!)) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Not enough stock available for item ${item.inventory_item_id} at location ${item.location_id}`
)
}
}
}
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?: BigNumberInput
})[]
): 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
})
}
// @ts-expect-error
async createReservationItems(
input: InventoryTypes.CreateReservationItemInput[],
context?: Context
): Promise<InventoryTypes.ReservationItemDTO[]>
// @ts-expect-error
async createReservationItems(
input: InventoryTypes.CreateReservationItemInput,
context?: Context
): Promise<InventoryTypes.ReservationItemDTO>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async createReservationItems(
input:
| InventoryTypes.CreateReservationItemInput[]
| InventoryTypes.CreateReservationItemInput,
@MedusaContext() context: Context = {}
): Promise<
InventoryTypes.ReservationItemDTO[] | InventoryTypes.ReservationItemDTO
> {
const toCreate = Array.isArray(input) ? input : [input]
const created = await this.createReservationItems_(toCreate, context)
const serializedReservations = await this.baseRepository_.serialize<
InventoryTypes.ReservationItemDTO[] | InventoryTypes.ReservationItemDTO
>(created)
return Array.isArray(input)
? serializedReservations
: serializedReservations[0]
}
@InjectTransactionManager()
async createReservationItems_(
input: InventoryTypes.CreateReservationItemInput[],
@MedusaContext() context: Context = {}
): Promise<InferEntityType<typeof ReservationItem>[]> {
const inventoryLevels = await this.ensureInventoryLevels(
input.map(
({ location_id, inventory_item_id, quantity, allow_backorder }) => ({
location_id,
inventory_item_id,
quantity,
allow_backorder,
})
),
{
validateQuantityAtLocation: true,
},
context
)
const created = await this.reservationItemService_.create(input, context)
const adjustments: Map<string, Map<string, number>> = input.reduce(
(acc, curr) => {
const locationMap = acc.get(curr.inventory_item_id) ?? new Map()
const adjustment = locationMap.get(curr.location_id) ?? 0
locationMap.set(curr.location_id, MathBN.add(adjustment, curr.quantity))
acc.set(curr.inventory_item_id, locationMap)
return acc
},
new Map()
)
const levelAdjustmentUpdates = inventoryLevels.map((level) => {
const adjustment = adjustments
.get(level.inventory_item_id)
?.get(level.location_id)
if (!adjustment) {
return
}
return {
id: level.id,
reserved_quantity: MathBN.add(level.reserved_quantity, adjustment),
}
})
await this.inventoryLevelService_.update(levelAdjustmentUpdates, context)
return created
}
// @ts-expect-error
createInventoryItems(
input: InventoryTypes.CreateInventoryItemInput,
context?: Context
): Promise<InventoryTypes.InventoryItemDTO>
createInventoryItems(
input: InventoryTypes.CreateInventoryItemInput[],
context?: Context
): Promise<InventoryTypes.InventoryItemDTO[]>
@InjectManager()
@EmitEvents()
async createInventoryItems(
input:
| InventoryTypes.CreateInventoryItemInput
| InventoryTypes.CreateInventoryItemInput[],
@MedusaContext() context: Context = {}
): Promise<
InventoryTypes.InventoryItemDTO | InventoryTypes.InventoryItemDTO[]
> {
const toCreate = this.sanitizeInventoryItemInput(
Array.isArray(input) ? input : [input]
)
const result = await this.createInventoryItems_(toCreate, context)
const serializedItems = await this.baseRepository_.serialize<
InventoryTypes.InventoryItemDTO | InventoryTypes.InventoryItemDTO[]
>(result)
return Array.isArray(input) ? serializedItems : serializedItems[0]
}
@InjectTransactionManager()
async createInventoryItems_(
input: InventoryTypes.CreateInventoryItemInput[],
@MedusaContext() context: Context = {}
): Promise<InventoryTypes.InventoryItemDTO[]> {
return await this.inventoryItemService_.create(input, context)
}
// @ts-ignore
createInventoryLevels(
input: InventoryTypes.CreateInventoryLevelInput,
context?: Context
): Promise<InventoryTypes.InventoryLevelDTO>
// @ts-expect-error
createInventoryLevels(
input: InventoryTypes.CreateInventoryLevelInput[],
context?: Context
): Promise<InventoryTypes.InventoryLevelDTO[]>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async createInventoryLevels(
input:
| InventoryTypes.CreateInventoryLevelInput[]
| InventoryTypes.CreateInventoryLevelInput,
@MedusaContext() context: Context = {}
): Promise<
InventoryTypes.InventoryLevelDTO[] | InventoryTypes.InventoryLevelDTO
> {
const toCreate = this.sanitizeInventoryLevelInput(
Array.isArray(input) ? input : [input]
)
const created = await this.createInventoryLevels_(toCreate, context)
const serialized = await this.baseRepository_.serialize<
InventoryTypes.InventoryLevelDTO[] | InventoryTypes.InventoryLevelDTO
>(created)
return Array.isArray(input) ? serialized : serialized[0]
}
@InjectTransactionManager()
async createInventoryLevels_(
input: InventoryTypes.CreateInventoryLevelInput[],
@MedusaContext() context: Context = {}
): Promise<InferEntityType<typeof InventoryLevel>[]> {
return await this.inventoryLevelService_.create(input, context)
}
// @ts-expect-error
updateInventoryItems(
input: InventoryTypes.UpdateInventoryItemInput[],
context?: Context
): Promise<InventoryTypes.InventoryItemDTO[]>
// @ts-expect-error
updateInventoryItems(
input: InventoryTypes.UpdateInventoryItemInput,
context?: Context
): Promise<InventoryTypes.InventoryItemDTO>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async updateInventoryItems(
input:
| InventoryTypes.UpdateInventoryItemInput
| InventoryTypes.UpdateInventoryItemInput[],
@MedusaContext() context: Context = {}
): Promise<
InventoryTypes.InventoryItemDTO | InventoryTypes.InventoryItemDTO[]
> {
const updates = this.sanitizeInventoryItemInput(
Array.isArray(input) ? input : [input]
)
const result = await this.updateInventoryItems_(updates, context)
const serializedItems = await this.baseRepository_.serialize<
InventoryTypes.InventoryItemDTO | InventoryTypes.InventoryItemDTO[]
>(result)
return Array.isArray(input) ? serializedItems : serializedItems[0]
}
@InjectTransactionManager()
async updateInventoryItems_(
input: (Partial<InventoryTypes.CreateInventoryItemInput> & {
id: string
})[],
@MedusaContext() context: Context = {}
): Promise<InferEntityType<typeof InventoryItem>[]> {
return await this.inventoryItemService_.update(input, context)
}
@InjectManager()
@EmitEvents()
async deleteInventoryItemLevelByLocationId(
locationId: string | string[],
@MedusaContext() context: Context = {}
): Promise<[object[], Record<string, unknown[]>]> {
const result = await this.inventoryLevelService_.softDelete(
{ location_id: locationId },
context
)
return result
}
/**
* Deletes an inventory level
* @param inventoryItemId - the id of the inventory item associated with the level
* @param locationId - the id of the location associated with the level
* @param context
*/
@InjectTransactionManager()
@EmitEvents()
async deleteInventoryLevel(
inventoryItemId: string,
locationId: string,
@MedusaContext() context: Context = {}
): Promise<void> {
const [inventoryLevel] = await this.inventoryLevelService_.list(
{ inventory_item_id: inventoryItemId, location_id: locationId },
{ take: 1 },
context
)
if (!inventoryLevel) {
return
}
await this.inventoryLevelService_.delete(inventoryLevel.id, context)
}
// @ts-ignore
async updateInventoryLevels(
updates: InventoryTypes.UpdateInventoryLevelInput[],
context?: Context
): Promise<InventoryTypes.InventoryLevelDTO[]>
// @ts-expect-error
async updateInventoryLevels(
updates: InventoryTypes.UpdateInventoryLevelInput,
context?: Context
): Promise<InventoryTypes.InventoryLevelDTO>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async updateInventoryLevels(
updates:
| InventoryTypes.UpdateInventoryLevelInput[]
| InventoryTypes.UpdateInventoryLevelInput,
@MedusaContext() context: Context = {}
): Promise<
InventoryTypes.InventoryLevelDTO | InventoryTypes.InventoryLevelDTO[]
> {
const input = this.sanitizeInventoryLevelInput(
Array.isArray(updates) ? updates : [updates]
)
const levels = await this.updateInventoryLevels_(input, context)
const updatedLevels = await this.baseRepository_.serialize<
InventoryTypes.InventoryLevelDTO | InventoryTypes.InventoryLevelDTO[]
>(levels)
return Array.isArray(updates) ? updatedLevels : updatedLevels[0]
}
@InjectTransactionManager()
async updateInventoryLevels_(
updates: InventoryTypes.UpdateInventoryLevelInput[],
@MedusaContext() context: Context = {}
) {
const inventoryLevels = await this.ensureInventoryLevels(
updates.map(({ location_id, inventory_item_id }) => ({
location_id,
inventory_item_id,
})),
undefined,
context
)
const levelMap = inventoryLevels.reduce((acc, curr) => {
const inventoryLevelMap = acc.get(curr.inventory_item_id) ?? new Map()
inventoryLevelMap.set(curr.location_id, curr.id)
acc.set(curr.inventory_item_id, inventoryLevelMap)
return acc
}, new Map())
const updatesWithIds = updates.map((update) => {
const id = levelMap.get(update.inventory_item_id).get(update.location_id)
return { id, ...update }
})
return await this.inventoryLevelService_.update(updatesWithIds, context)
}
/**
* Updates a reservation item
* @param reservationItemId
* @param input - the input object
* @param context
* @param context
* @return The updated inventory level
*/
// @ts-expect-error
async updateReservationItems(
input: InventoryTypes.UpdateReservationItemInput[],
context?: Context
): Promise<InventoryTypes.ReservationItemDTO[]>
// @ts-expect-error
async updateReservationItems(
input: InventoryTypes.UpdateReservationItemInput,
context?: Context
): Promise<InventoryTypes.ReservationItemDTO>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async updateReservationItems(
input:
| InventoryTypes.UpdateReservationItemInput
| InventoryTypes.UpdateReservationItemInput[],
@MedusaContext() context: Context = {}
): Promise<
InventoryTypes.ReservationItemDTO | InventoryTypes.ReservationItemDTO[]
> {
const update = Array.isArray(input) ? input : [input]
const result = await this.updateReservationItems_(update, context)
const serialized = await this.baseRepository_.serialize<
InventoryTypes.ReservationItemDTO | InventoryTypes.ReservationItemDTO[]
>(result)
return Array.isArray(input) ? serialized : serialized[0]
}
@InjectTransactionManager()
async updateReservationItems_(
input: (InventoryTypes.UpdateReservationItemInput & { id: string })[],
@MedusaContext() context: Context = {}
): Promise<InferEntityType<typeof ReservationItem>[]> {
const ids = input.map((u) => u.id)
const reservationItems = await this.reservationItemService_.list(
{ 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<string, ReservationItemDTO> = new Map(
reservationItems.map((r) => [r.id, r])
)
const adjustments: Map<string, Map<string, number>> = input.reduce(
(acc, update) => {
const reservation = reservationMap.get(update.id)!
const locationMap = acc.get(reservation.inventory_item_id) ?? new Map()
if (
isDefined(update.location_id) &&
update.location_id !== reservation.location_id
) {
const reservationLocationAdjustment =
locationMap.get(reservation.location_id) ?? 0
locationMap.set(
reservation.location_id,
MathBN.sub(reservationLocationAdjustment, reservation.quantity)
)
const updateLocationAdjustment =
locationMap.get(update.location_id) ?? 0
locationMap.set(
update.location_id,
MathBN.add(
updateLocationAdjustment,
update.quantity || reservation.quantity
)
)
} else if (
isDefined(update.quantity) &&
!MathBN.eq(update.quantity, reservation.quantity)
) {
const locationAdjustment =
locationMap.get(reservation.location_id) ?? 0
locationMap.set(
reservation.location_id,
MathBN.add(
locationAdjustment,
MathBN.sub(update.quantity!, reservation.quantity)
)
)
}
acc.set(reservation.inventory_item_id, locationMap)
return acc
},
new Map()
)
const availabilityData = input.map((data) => {
const reservation = reservationMap.get(data.id)!
let adjustment = data.quantity
? MathBN.sub(data.quantity, reservation.quantity)
: 0
if (MathBN.lt(adjustment, 0)) {
adjustment = 0
}
return {
inventory_item_id: reservation.inventory_item_id,
location_id: data.location_id ?? reservation.location_id,
quantity: adjustment,
allow_backorder:
data.allow_backorder || reservation.allow_backorder || false,
}
})
const inventoryLevels = await this.ensureInventoryLevels(
availabilityData,
{
validateQuantityAtLocation: true,
},
context
)
const result = await this.reservationItemService_.update(input, context)
const levelAdjustmentUpdates = inventoryLevels
.map((level) => {
const adjustment = adjustments
.get(level.inventory_item_id)
?.get(level.location_id)
if (!adjustment) {
return
}
return {
id: level.id,
reserved_quantity: MathBN.add(level.reserved_quantity, adjustment),
}
})
.filter(Boolean)
await this.inventoryLevelService_.update(levelAdjustmentUpdates, context)
return result
}
@InjectManager()
@EmitEvents()
// @ts-expect-error
async softDeleteReservationItems(
ids: string | string[],
config?: SoftDeleteReturn<string>,
@MedusaContext() context: Context = {}
): Promise<void> {
return await this.softDeleteReservationItems_(ids, config, context)
}
@InjectTransactionManager()
protected async softDeleteReservationItems_(
ids: string | string[],
config?: SoftDeleteReturn<string>,
@MedusaContext() context: Context = {}
): Promise<void> {
const reservations: InventoryTypes.ReservationItemDTO[] =
await this.reservationItemService_.list({ id: ids }, {}, context)
await super.softDeleteReservationItems(ids, config, context)
await this.adjustInventoryLevelsForReservationsDeletion(
reservations,
context
)
}
@InjectManager()
@EmitEvents()
// @ts-expect-error
async restoreReservationItems(
ids: string | string[],
config?: RestoreReturn<string>,
@MedusaContext() context: Context = {}
): Promise<void> {
return await this.restoreReservationItems_(ids, config, context)
}
@InjectTransactionManager()
protected async restoreReservationItems_(
ids: string | string[],
config?: RestoreReturn<string>,
@MedusaContext() context: Context = {}
): Promise<void> {
const reservations: InventoryTypes.ReservationItemDTO[] =
await super.listReservationItems({ id: ids }, {}, context)
await super.restoreReservationItems({ id: ids }, config, context)
await this.adjustInventoryLevelsForReservationsRestore(
reservations,
context
)
}
@InjectManager()
@EmitEvents()
async deleteReservationItemByLocationId(
locationId: string | string[],
@MedusaContext() context: Context = {}
): Promise<void> {
return await this.deleteReservationItemByLocationId_(locationId, context)
}
@InjectTransactionManager()
protected async deleteReservationItemByLocationId_(
locationId: string | string[],
@MedusaContext() context: Context = {}
): Promise<void> {
const reservations: InventoryTypes.ReservationItemDTO[] =
await this.reservationItemService_.list(
{ location_id: locationId },
{},
context
)
await this.reservationItemService_.softDelete(
{ location_id: locationId },
context
)
await this.adjustInventoryLevelsForReservationsDeletion(
reservations,
context
)
}
/**
* Deletes reservation items by line item
* @param lineItemId - the id of the line item associated with the reservation item
* @param context
*/
@InjectManager()
@EmitEvents()
async deleteReservationItemsByLineItem(
lineItemId: string | string[],
@MedusaContext() context: Context = {}
): Promise<void> {
return await this.deleteReservationItemsByLineItem_(lineItemId, context)
}
@InjectTransactionManager()
protected async deleteReservationItemsByLineItem_(
lineItemId: string | string[],
@MedusaContext() context: Context = {}
): Promise<void> {
const reservations: InventoryTypes.ReservationItemDTO[] =
await this.reservationItemService_.list(
{ line_item_id: lineItemId },
{},
context
)
await this.reservationItemService_.softDelete(
{ line_item_id: lineItemId },
context
)
await this.adjustInventoryLevelsForReservationsDeletion(
reservations,
context
)
}
/**
* Deletes reservation items by line item
* @param lineItemId - the id of the line item associated with the reservation item
* @param context
*/
@InjectManager()
@EmitEvents()
async restoreReservationItemsByLineItem(
lineItemId: string | string[],
@MedusaContext() context: Context = {}
): Promise<void> {
return await this.restoreReservationItemsByLineItem_(lineItemId, context)
}
@InjectTransactionManager()
protected async restoreReservationItemsByLineItem_(
lineItemId: string | string[],
@MedusaContext() context: Context = {}
): Promise<void> {
const reservations: InventoryTypes.ReservationItemDTO[] =
await this.reservationItemService_.list(
{ line_item_id: lineItemId },
{},
context
)
await this.reservationItemService_.restore(
{ line_item_id: lineItemId },
context
)
await this.adjustInventoryLevelsForReservationsRestore(
reservations,
context
)
}
/**
* Adjusts the inventory level for a given inventory item and location.
* @param inventoryItemId - the id of the inventory item
* @param locationId - the id of the location
* @param adjustment - the number to adjust the inventory by (can be positive or negative)
* @param context
* @return The updated inventory level
* @throws when the inventory level is not found
*/
adjustInventory(
inventoryItemId: string,
locationId: string,
adjustment: BigNumberInput,
context: Context
): Promise<InventoryTypes.InventoryLevelDTO>
adjustInventory(
data: {
inventoryItemId: string
locationId: string
adjustment: BigNumberInput
}[],
context: Context
): Promise<InventoryTypes.InventoryLevelDTO[]>
@InjectManager()
@EmitEvents()
async adjustInventory(
inventoryItemIdOrData: string | any,
locationId?: string | Context,
adjustment?: BigNumberInput,
@MedusaContext() context: Context = {}
): Promise<
InventoryTypes.InventoryLevelDTO | InventoryTypes.InventoryLevelDTO[]
> {
let all: any = inventoryItemIdOrData
if (isString(inventoryItemIdOrData)) {
all = [
{
inventoryItemId: inventoryItemIdOrData,
locationId,
adjustment,
},
]
}
const results: InferEntityType<typeof InventoryLevel>[] = []
for (const data of all) {
const result = await this.adjustInventory_(
data.inventoryItemId,
data.locationId,
data.adjustment,
context
)
results.push(result)
}
return await this.baseRepository_.serialize<InventoryTypes.InventoryLevelDTO>(
Array.isArray(inventoryItemIdOrData) ? results : results[0]
)
}
@InjectTransactionManager()
async adjustInventory_(
inventoryItemId: string,
locationId: string,
adjustment: BigNumberInput,
@MedusaContext() context: Context = {}
): Promise<InferEntityType<typeof InventoryLevel>> {
const [inventoryLevel] = await this.inventoryLevelService_.list(
{ inventory_item_id: inventoryItemId, location_id: locationId },
{},
context
)
if (!inventoryLevel) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Inventory level for item ${inventoryItemId} and location ${locationId} not found`
)
}
const result = await this.inventoryLevelService_.update(
{
id: inventoryLevel.id,
stocked_quantity: MathBN.add(
inventoryLevel.stocked_quantity,
adjustment
),
},
context
)
return result
}
@InjectManager()
async retrieveInventoryLevelByItemAndLocation(
inventoryItemId: string,
locationId: string,
@MedusaContext() context: Context = {}
): Promise<InventoryTypes.InventoryLevelDTO> {
const [inventoryLevel] = await this.listInventoryLevels(
{ inventory_item_id: inventoryItemId, location_id: locationId },
{},
context
)
if (!inventoryLevel) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Inventory level for item ${inventoryItemId} and location ${locationId} not found`
)
}
return inventoryLevel
}
/**
* Retrieves the available quantity of a given inventory item in a given location.
* @param inventoryItemId - the id of the inventory item
* @param locationIds - the ids of the locations to check
* @param context
* @return The available quantity
* @throws when the inventory item is not found
*/
@InjectManager()
async retrieveAvailableQuantity(
inventoryItemId: string,
locationIds: string[],
@MedusaContext() context: Context = {}
): Promise<BigNumber> {
if (locationIds.length === 0) {
return new BigNumber(0)
}
await this.inventoryItemService_.retrieve(
inventoryItemId,
{
select: ["id"],
},
context
)
const availableQuantity =
await this.inventoryLevelService_.getAvailableQuantity(
inventoryItemId,
locationIds,
context
)
return availableQuantity
}
/**
* Retrieves the stocked quantity of a given inventory item in a given location.
* @param inventoryItemId - the id of the inventory item
* @param locationIds - the ids of the locations to check
* @param context
* @return The stocked quantity
* @throws when the inventory item is not found
*/
@InjectManager()
async retrieveStockedQuantity(
inventoryItemId: string,
locationIds: string[],
@MedusaContext() context: Context = {}
): Promise<BigNumber> {
if (locationIds.length === 0) {
return new BigNumber(0)
}
// Throws if item does not exist
await this.inventoryItemService_.retrieve(
inventoryItemId,
{
select: ["id"],
},
context
)
const stockedQuantity =
await this.inventoryLevelService_.retrieveStockedQuantity(
inventoryItemId,
locationIds,
context
)
return stockedQuantity
}
/**
* Retrieves the reserved quantity of a given inventory item in a given location.
* @param inventoryItemId - the id of the inventory item
* @param locationIds - the ids of the locations to check
* @param context
* @return The reserved quantity
* @throws when the inventory item is not found
*/
@InjectManager()
async retrieveReservedQuantity(
inventoryItemId: string,
locationIds: string[],
@MedusaContext() context: Context = {}
): Promise<BigNumber> {
// Throws if item does not exist
await this.inventoryItemService_.retrieve(
inventoryItemId,
{
select: ["id"],
},
context
)
if (locationIds.length === 0) {
return new BigNumber(0)
}
const reservedQuantity =
await this.inventoryLevelService_.getReservedQuantity(
inventoryItemId,
locationIds,
context
)
return reservedQuantity
}
/**
* Confirms whether there is sufficient inventory for a given quantity of a given inventory item in a given location.
* @param inventoryItemId - the id of the inventory item
* @param locationIds - the ids of the locations to check
* @param quantity - the quantity to check
* @param context
* @return Whether there is sufficient inventory
*/
@InjectManager()
async confirmInventory(
inventoryItemId: string,
locationIds: string[],
quantity: BigNumberInput,
@MedusaContext() context: Context = {}
): Promise<boolean> {
const availableQuantity = await this.retrieveAvailableQuantity(
inventoryItemId,
locationIds,
context
)
return MathBN.gte(availableQuantity, quantity)
}
private async adjustInventoryLevelsForReservationsDeletion(
reservations: ReservationItemDTO[],
context: Context
): Promise<void> {
await this.adjustInventoryLevelsForReservations_(
reservations,
true,
context
)
}
private async adjustInventoryLevelsForReservationsRestore(
reservations: ReservationItemDTO[],
context: Context
): Promise<void> {
await this.adjustInventoryLevelsForReservations_(
reservations,
false,
context
)
}
private async adjustInventoryLevelsForReservations_(
reservations: ReservationItemDTO[],
isDelete: boolean,
context: Context
): Promise<void> {
const multiplier = isDelete ? -1 : 1
const inventoryLevels = await this.ensureInventoryLevels(
reservations.map((r) => ({
inventory_item_id: r.inventory_item_id,
location_id: r.location_id,
})),
undefined,
context
)
const inventoryLevelAdjustments: Map<
string,
Map<string, number>
> = reservations.reduce((acc, curr) => {
const inventoryLevelMap = acc.get(curr.inventory_item_id) ?? new Map()
const adjustment = inventoryLevelMap.has(curr.location_id)
? MathBN.add(
inventoryLevelMap.get(curr.location_id),
MathBN.mult(curr.quantity, multiplier)
)
: MathBN.mult(curr.quantity, multiplier)
inventoryLevelMap.set(curr.location_id, adjustment)
acc.set(curr.inventory_item_id, inventoryLevelMap)
return acc
}, new Map())
const levelAdjustmentUpdates = inventoryLevels.map((level) => {
const adjustment = inventoryLevelAdjustments
.get(level.inventory_item_id)
?.get(level.location_id)
if (!adjustment) {
return
}
return {
id: level.id,
reserved_quantity: MathBN.add(level.reserved_quantity, adjustment),
}
})
await this.inventoryLevelService_.update(levelAdjustmentUpdates, context)
}
}