Feat/bulk operations for inventory service (#4503)
* initial push * bulk delete reservations by location ids * add method to interface (not implemented yet) * bulk update * delete reservations by location id bulk * add create bulk for inventory item * refactor attach inventory item method * add changeset * verbose false * method override instead of multiple methods * change up method signature * redo changes when updating interface * update createInventoryLevel method * rename variables * fix feedback * return correct string array when emitting event * refactor inventory service * redo order changes * snapshot * move prep methods
This commit is contained in:
@@ -12,7 +12,7 @@ import {
|
||||
MedusaContext,
|
||||
MedusaError,
|
||||
} from "@medusajs/utils"
|
||||
import { DeepPartial, EntityManager, FindManyOptions } from "typeorm"
|
||||
import { DeepPartial, EntityManager, FindManyOptions, In } from "typeorm"
|
||||
import { InventoryItem } from "../models"
|
||||
import { getListQuery } from "../utils/query"
|
||||
import { buildQuery } from "../utils/build-query"
|
||||
@@ -120,33 +120,35 @@ export default class InventoryItemService {
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
async create(
|
||||
data: CreateInventoryItemInput,
|
||||
data: CreateInventoryItemInput[],
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
): Promise<InventoryItem> {
|
||||
): Promise<InventoryItem[]> {
|
||||
const manager = context.transactionManager!
|
||||
const itemRepository = manager.getRepository(InventoryItem)
|
||||
|
||||
const inventoryItem = itemRepository.create({
|
||||
sku: data.sku,
|
||||
origin_country: data.origin_country,
|
||||
metadata: data.metadata,
|
||||
hs_code: data.hs_code,
|
||||
mid_code: data.mid_code,
|
||||
material: data.material,
|
||||
weight: data.weight,
|
||||
length: data.length,
|
||||
height: data.height,
|
||||
width: data.width,
|
||||
requires_shipping: data.requires_shipping,
|
||||
description: data.description,
|
||||
thumbnail: data.thumbnail,
|
||||
title: data.title,
|
||||
})
|
||||
const inventoryItem = itemRepository.create(
|
||||
data.map((tc) => ({
|
||||
sku: tc.sku,
|
||||
origin_country: tc.origin_country,
|
||||
metadata: tc.metadata,
|
||||
hs_code: tc.hs_code,
|
||||
mid_code: tc.mid_code,
|
||||
material: tc.material,
|
||||
weight: tc.weight,
|
||||
length: tc.length,
|
||||
height: tc.height,
|
||||
width: tc.width,
|
||||
requires_shipping: tc.requires_shipping,
|
||||
description: tc.description,
|
||||
thumbnail: tc.thumbnail,
|
||||
title: tc.title,
|
||||
}))
|
||||
)
|
||||
|
||||
const result = await itemRepository.save(inventoryItem)
|
||||
|
||||
await this.eventBusService_?.emit?.(InventoryItemService.Events.CREATED, {
|
||||
id: result.id,
|
||||
ids: result.map((i) => i.id),
|
||||
})
|
||||
|
||||
return result
|
||||
@@ -195,16 +197,20 @@ export default class InventoryItemService {
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
async delete(
|
||||
inventoryItemId: string,
|
||||
inventoryItemId: string | string[],
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
): Promise<void> {
|
||||
const manager = context.transactionManager!
|
||||
const itemRepository = manager.getRepository(InventoryItem)
|
||||
|
||||
await itemRepository.softRemove({ id: inventoryItemId })
|
||||
const ids = Array.isArray(inventoryItemId)
|
||||
? inventoryItemId
|
||||
: [inventoryItemId]
|
||||
|
||||
await itemRepository.softDelete({ id: In(ids) })
|
||||
|
||||
await this.eventBusService_?.emit?.(InventoryItemService.Events.DELETED, {
|
||||
id: inventoryItemId,
|
||||
ids: inventoryItemId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,24 +120,28 @@ export default class InventoryLevelService {
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
async create(
|
||||
data: CreateInventoryLevelInput,
|
||||
data: CreateInventoryLevelInput[],
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
): Promise<InventoryLevel> {
|
||||
): Promise<InventoryLevel[]> {
|
||||
const manager = context.transactionManager!
|
||||
|
||||
const toCreate = data.map((d) => {
|
||||
return {
|
||||
location_id: d.location_id,
|
||||
inventory_item_id: d.inventory_item_id,
|
||||
stocked_quantity: d.stocked_quantity,
|
||||
reserved_quantity: d.reserved_quantity,
|
||||
incoming_quantity: d.incoming_quantity,
|
||||
}
|
||||
})
|
||||
|
||||
const levelRepository = manager.getRepository(InventoryLevel)
|
||||
|
||||
const inventoryLevel = levelRepository.create({
|
||||
location_id: data.location_id,
|
||||
inventory_item_id: data.inventory_item_id,
|
||||
stocked_quantity: data.stocked_quantity,
|
||||
reserved_quantity: data.reserved_quantity,
|
||||
incoming_quantity: data.incoming_quantity,
|
||||
})
|
||||
const inventoryLevels = levelRepository.create(toCreate)
|
||||
|
||||
const saved = await levelRepository.save(inventoryLevel)
|
||||
const saved = await levelRepository.save(inventoryLevels)
|
||||
await this.eventBusService_?.emit?.(InventoryLevelService.Events.CREATED, {
|
||||
id: saved.id,
|
||||
ids: saved.map((i) => i.id),
|
||||
})
|
||||
|
||||
return saved
|
||||
@@ -254,7 +258,7 @@ export default class InventoryLevelService {
|
||||
await levelRepository.delete({ id: In(ids) })
|
||||
|
||||
await this.eventBusService_?.emit?.(InventoryLevelService.Events.DELETED, {
|
||||
id: inventoryLevelId,
|
||||
ids: inventoryLevelId,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -265,16 +269,18 @@ export default class InventoryLevelService {
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
async deleteByLocationId(
|
||||
locationId: string,
|
||||
locationId: string | string[],
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
): Promise<void> {
|
||||
const manager = context.transactionManager!
|
||||
const levelRepository = manager.getRepository(InventoryLevel)
|
||||
|
||||
await levelRepository.delete({ location_id: locationId })
|
||||
const ids = Array.isArray(locationId) ? locationId : [locationId]
|
||||
|
||||
await levelRepository.delete({ location_id: In(ids) })
|
||||
|
||||
await this.eventBusService_?.emit?.(InventoryLevelService.Events.DELETED, {
|
||||
location_id: locationId,
|
||||
location_ids: ids,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { InternalModuleDeclaration } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
BulkUpdateInventoryLevelInput,
|
||||
CreateInventoryItemInput,
|
||||
CreateInventoryLevelInput,
|
||||
CreateReservationItemInput,
|
||||
@@ -32,6 +33,7 @@ type InjectedDependencies = {
|
||||
inventoryLevelService: InventoryLevelService
|
||||
reservationItemService: ReservationItemService
|
||||
}
|
||||
|
||||
export default class InventoryService implements IInventoryService {
|
||||
protected readonly manager_: EntityManager
|
||||
|
||||
@@ -184,10 +186,63 @@ export default class InventoryService implements IInventoryService {
|
||||
)
|
||||
}
|
||||
|
||||
private async ensureInventoryLevels(
|
||||
data: { location_id: string; inventory_item_id: string }[],
|
||||
context: SharedContext = {}
|
||||
): Promise<InventoryLevelDTO[]> {
|
||||
const inventoryLevels = await this.inventoryLevelService_.list(
|
||||
{
|
||||
inventory_item_id: data.map((e) => e.inventory_item_id),
|
||||
location_id: data.map((e) => e.location_id),
|
||||
},
|
||||
{},
|
||||
context
|
||||
)
|
||||
|
||||
const inventoryLevelMap: Map<
|
||||
string,
|
||||
Map<string, 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(
|
||||
(i) => !inventoryLevelMap.get(i.inventory_item_id)?.get(i.location_id)
|
||||
)
|
||||
|
||||
if (missing.length) {
|
||||
const error = missing
|
||||
.map((missing) => {
|
||||
return `Item ${missing.inventory_item_id} is not stocked at location ${missing.location_id}`
|
||||
})
|
||||
.join(", ")
|
||||
throw new MedusaError(MedusaError.Types.NOT_FOUND, error)
|
||||
}
|
||||
|
||||
return inventoryLevels.map(
|
||||
(i) => inventoryLevelMap.get(i.inventory_item_id)!.get(i.location_id)!
|
||||
)
|
||||
}
|
||||
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async createReservationItems(
|
||||
input: CreateReservationItemInput[],
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
): Promise<ReservationItemDTO[]> {
|
||||
await this.ensureInventoryLevels(input, context)
|
||||
|
||||
return await this.reservationItemService_.create(input, context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a reservation item
|
||||
* @param input - the input object
|
||||
* @param context
|
||||
* @return The created reservation item
|
||||
*/
|
||||
@InjectEntityManager(
|
||||
@@ -198,29 +253,20 @@ export default class InventoryService implements IInventoryService {
|
||||
input: CreateReservationItemInput,
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
): Promise<ReservationItemDTO> {
|
||||
// Verify that the item is stocked at the location
|
||||
const [inventoryLevel] = await this.inventoryLevelService_.list(
|
||||
{
|
||||
inventory_item_id: input.inventory_item_id,
|
||||
location_id: input.location_id,
|
||||
},
|
||||
{ take: 1 },
|
||||
context
|
||||
)
|
||||
const [result] = await this.createReservationItems([input], context)
|
||||
|
||||
if (!inventoryLevel) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Item ${input.inventory_item_id} is not stocked at location ${input.location_id}`
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const reservationItem = await this.reservationItemService_.create(
|
||||
input,
|
||||
context
|
||||
)
|
||||
|
||||
return { ...reservationItem }
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async createInventoryItems(
|
||||
input: CreateInventoryItemInput[],
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
): Promise<InventoryItemDTO[]> {
|
||||
return await this.inventoryItemService_.create(input, context)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -237,11 +283,20 @@ export default class InventoryService implements IInventoryService {
|
||||
input: CreateInventoryItemInput,
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
): Promise<InventoryItemDTO> {
|
||||
const inventoryItem = await this.inventoryItemService_.create(
|
||||
input,
|
||||
context
|
||||
)
|
||||
return { ...inventoryItem }
|
||||
const [result] = await this.createInventoryItems([input], context)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async createInventoryLevels(
|
||||
input: CreateInventoryLevelInput[],
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
): Promise<InventoryLevelDTO[]> {
|
||||
return await this.inventoryLevelService_.create(input, context)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -258,7 +313,9 @@ export default class InventoryService implements IInventoryService {
|
||||
input: CreateInventoryLevelInput,
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
): Promise<InventoryLevelDTO> {
|
||||
return await this.inventoryLevelService_.create(input, context)
|
||||
const [result] = await this.createInventoryLevels([input], context)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,7 +352,7 @@ export default class InventoryService implements IInventoryService {
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async deleteInventoryItem(
|
||||
inventoryItemId: string,
|
||||
inventoryItemId: string | string[],
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
): Promise<void> {
|
||||
await this.inventoryLevelService_.deleteByInventoryItemId(
|
||||
@@ -311,7 +368,7 @@ export default class InventoryService implements IInventoryService {
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async deleteInventoryItemLevelByLocationId(
|
||||
locationId: string,
|
||||
locationId: string | string[],
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
): Promise<void> {
|
||||
return await this.inventoryLevelService_.deleteByLocationId(
|
||||
@@ -325,7 +382,7 @@ export default class InventoryService implements IInventoryService {
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async deleteReservationItemByLocationId(
|
||||
locationId: string,
|
||||
locationId: string | string[],
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
): Promise<void> {
|
||||
return await this.reservationItemService_.deleteByLocationId(
|
||||
@@ -362,6 +419,38 @@ export default class InventoryService implements IInventoryService {
|
||||
return await this.inventoryLevelService_.delete(inventoryLevel.id, context)
|
||||
}
|
||||
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async updateInventoryLevels(
|
||||
updates: ({
|
||||
inventory_item_id: string
|
||||
location_id: string
|
||||
} & UpdateInventoryLevelInput)[],
|
||||
context?: SharedContext
|
||||
): Promise<InventoryLevelDTO[]> {
|
||||
const inventoryLevels = await this.ensureInventoryLevels(updates)
|
||||
|
||||
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())
|
||||
|
||||
return await Promise.all(
|
||||
updates.map(async (update) => {
|
||||
const levelId = levelMap
|
||||
.get(update.inventory_item_id)
|
||||
.get(update.location_id)
|
||||
|
||||
// TODO make this bulk
|
||||
return this.inventoryLevelService_.update(levelId, update, context)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an inventory level
|
||||
* @param inventoryItemId - the id of the inventory item associated with the level
|
||||
@@ -376,28 +465,21 @@ export default class InventoryService implements IInventoryService {
|
||||
)
|
||||
async updateInventoryLevel(
|
||||
inventoryItemId: string,
|
||||
locationId: string,
|
||||
input: UpdateInventoryLevelInput,
|
||||
locationIdOrContext?: string,
|
||||
input?: UpdateInventoryLevelInput,
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
): Promise<InventoryLevelDTO> {
|
||||
const [inventoryLevel] = await this.inventoryLevelService_.list(
|
||||
{ inventory_item_id: inventoryItemId, location_id: locationId },
|
||||
{ take: 1 },
|
||||
context
|
||||
)
|
||||
const updates: BulkUpdateInventoryLevelInput[] = [
|
||||
{
|
||||
inventory_item_id: inventoryItemId,
|
||||
location_id: locationIdOrContext as string,
|
||||
...input,
|
||||
},
|
||||
]
|
||||
|
||||
if (!inventoryLevel) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Inventory level for item ${inventoryItemId} and location ${locationId} not found`
|
||||
)
|
||||
}
|
||||
const [result] = await this.updateInventoryLevels(updates, context)
|
||||
|
||||
return await this.inventoryLevelService_.update(
|
||||
inventoryLevel.id,
|
||||
input,
|
||||
context
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -131,38 +131,44 @@ export default class ReservationItemService {
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
async create(
|
||||
data: CreateReservationItemInput,
|
||||
data: CreateReservationItemInput[],
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
): Promise<ReservationItem> {
|
||||
): Promise<ReservationItem[]> {
|
||||
const manager = context.transactionManager!
|
||||
const reservationItemRepository = manager.getRepository(ReservationItem)
|
||||
|
||||
const reservationItem = reservationItemRepository.create({
|
||||
inventory_item_id: data.inventory_item_id,
|
||||
line_item_id: data.line_item_id,
|
||||
location_id: data.location_id,
|
||||
quantity: data.quantity,
|
||||
metadata: data.metadata,
|
||||
external_id: data.external_id,
|
||||
description: data.description,
|
||||
created_by: data.created_by,
|
||||
})
|
||||
const reservationItems = reservationItemRepository.create(
|
||||
data.map((tc) => ({
|
||||
inventory_item_id: tc.inventory_item_id,
|
||||
line_item_id: tc.line_item_id,
|
||||
location_id: tc.location_id,
|
||||
quantity: tc.quantity,
|
||||
metadata: tc.metadata,
|
||||
external_id: tc.external_id,
|
||||
description: tc.description,
|
||||
created_by: tc.created_by,
|
||||
}))
|
||||
)
|
||||
|
||||
const [newReservationItem] = await Promise.all([
|
||||
reservationItemRepository.save(reservationItem),
|
||||
this.inventoryLevelService_.adjustReservedQuantity(
|
||||
data.inventory_item_id,
|
||||
data.location_id,
|
||||
data.quantity,
|
||||
context
|
||||
const [newReservationItems] = await Promise.all([
|
||||
reservationItemRepository.save(reservationItems),
|
||||
...data.map(
|
||||
async (data) =>
|
||||
// TODO make bulk
|
||||
await this.inventoryLevelService_.adjustReservedQuantity(
|
||||
data.inventory_item_id,
|
||||
data.location_id,
|
||||
data.quantity,
|
||||
context
|
||||
)
|
||||
),
|
||||
])
|
||||
|
||||
await this.eventBusService_?.emit?.(ReservationItemService.Events.CREATED, {
|
||||
id: newReservationItem.id,
|
||||
ids: newReservationItems.map((i) => i.id),
|
||||
})
|
||||
|
||||
return newReservationItem
|
||||
return newReservationItems
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,24 +250,24 @@ export default class ReservationItemService {
|
||||
const manager = context.transactionManager!
|
||||
const itemRepository = manager.getRepository(ReservationItem)
|
||||
|
||||
const itemsIds = Array.isArray(lineItemId) ? lineItemId : [lineItemId]
|
||||
const lineItemIds = Array.isArray(lineItemId) ? lineItemId : [lineItemId]
|
||||
|
||||
const items = await this.list(
|
||||
{ line_item_id: itemsIds },
|
||||
const reservationItems = await this.list(
|
||||
{ line_item_id: lineItemIds },
|
||||
undefined,
|
||||
context
|
||||
)
|
||||
|
||||
const ops: Promise<unknown>[] = [
|
||||
itemRepository.softDelete({ line_item_id: In(itemsIds) }),
|
||||
itemRepository.softDelete({ line_item_id: In(lineItemIds) }),
|
||||
]
|
||||
|
||||
for (const item of items) {
|
||||
for (const reservation of reservationItems) {
|
||||
ops.push(
|
||||
this.inventoryLevelService_.adjustReservedQuantity(
|
||||
item.inventory_item_id,
|
||||
item.location_id,
|
||||
item.quantity * -1,
|
||||
reservation.inventory_item_id,
|
||||
reservation.location_id,
|
||||
reservation.quantity * -1,
|
||||
context
|
||||
)
|
||||
)
|
||||
@@ -281,18 +287,15 @@ export default class ReservationItemService {
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
async deleteByLocationId(
|
||||
locationId: string,
|
||||
locationId: string | string[],
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
): Promise<void> {
|
||||
const manager = context.transactionManager!
|
||||
const itemRepository = manager.getRepository(ReservationItem)
|
||||
|
||||
await itemRepository
|
||||
.createQueryBuilder("reservation_item")
|
||||
.softDelete()
|
||||
.where("location_id = :locationId", { locationId })
|
||||
.andWhere("deleted_at IS NULL")
|
||||
.execute()
|
||||
const ids = Array.isArray(locationId) ? locationId : [locationId]
|
||||
|
||||
await itemRepository.softDelete({ location_id: In(ids) })
|
||||
|
||||
await this.eventBusService_?.emit?.(ReservationItemService.Events.DELETED, {
|
||||
location_id: locationId,
|
||||
@@ -330,7 +333,7 @@ export default class ReservationItemService {
|
||||
await Promise.all(promises)
|
||||
|
||||
await this.eventBusService_?.emit?.(ReservationItemService.Events.DELETED, {
|
||||
id: reservationItemId,
|
||||
ids: reservationItemId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user