Files
medusa-store/packages/inventory/src/services/reservation-item.ts
T
Philip Korsholm d184d23c63 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
2023-07-18 11:17:57 +02:00

340 lines
9.9 KiB
TypeScript

import {
CreateReservationItemInput,
FilterableReservationItemProps,
FindConfig,
IEventBusService,
SharedContext,
UpdateReservationItemInput,
} from "@medusajs/types"
import {
InjectEntityManager,
isDefined,
MedusaContext,
MedusaError,
} from "@medusajs/utils"
import { EntityManager, FindManyOptions, In } from "typeorm"
import { InventoryLevelService } from "."
import { ReservationItem } from "../models"
import { buildQuery } from "../utils/build-query"
type InjectedDependencies = {
eventBusService: IEventBusService
manager: EntityManager
inventoryLevelService: InventoryLevelService
}
export default class ReservationItemService {
static Events = {
CREATED: "reservation-item.created",
UPDATED: "reservation-item.updated",
DELETED: "reservation-item.deleted",
}
protected readonly manager_: EntityManager
protected readonly eventBusService_: IEventBusService | undefined
protected readonly inventoryLevelService_: InventoryLevelService
constructor({
eventBusService,
inventoryLevelService,
manager,
}: InjectedDependencies) {
this.manager_ = manager
this.eventBusService_ = eventBusService
this.inventoryLevelService_ = inventoryLevelService
}
/**
* Lists reservation items that match the provided filter.
* @param selector - Filters to apply to the reservation items.
* @param config - Configuration for the query.
* @param context
* @return Array of reservation items that match the selector.
*/
async list(
selector: FilterableReservationItemProps = {},
config: FindConfig<ReservationItem> = { relations: [], skip: 0, take: 10 },
context: SharedContext = {}
): Promise<ReservationItem[]> {
const manager = context.transactionManager ?? this.manager_
const itemRepository = manager.getRepository(ReservationItem)
const query = buildQuery(selector, config) as FindManyOptions
return await itemRepository.find(query)
}
/**
* Lists reservation items that match the provided filter and returns the total count.
* @param selector - Filters to apply to the reservation items.
* @param config - Configuration for the query.
* @param context
* @return Array of reservation items that match the selector and the total count.
*/
async listAndCount(
selector: FilterableReservationItemProps = {},
config: FindConfig<ReservationItem> = { relations: [], skip: 0, take: 10 },
context: SharedContext = {}
): Promise<[ReservationItem[], number]> {
const manager = context.transactionManager ?? this.manager_
const itemRepository = manager.getRepository(ReservationItem)
const query = buildQuery(selector, config) as FindManyOptions
return await itemRepository.findAndCount(query)
}
/**
* Retrieves a reservation item by its id.
* @param reservationItemId - The id of the reservation item to retrieve.
* @param config - Configuration for the query.
* @param context
* @return The reservation item with the provided id.
* @throws If reservationItemId is not defined or if the reservation item was not found.
*/
async retrieve(
reservationItemId: string,
config: FindConfig<ReservationItem> = {},
context: SharedContext = {}
): Promise<ReservationItem> {
if (!isDefined(reservationItemId)) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`"reservationItemId" must be defined`
)
}
const manager = context.transactionManager ?? this.manager_
const reservationItemRepository = manager.getRepository(ReservationItem)
const query = buildQuery(
{ id: reservationItemId },
config
) as FindManyOptions
const [reservationItem] = await reservationItemRepository.find(query)
if (!reservationItem) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`ReservationItem with id ${reservationItemId} was not found`
)
}
return reservationItem
}
/**
* Create a new reservation item.
* @param data - The reservation item data.
* @param context
* @return The created reservation item.
*/
@InjectEntityManager()
async create(
data: CreateReservationItemInput[],
@MedusaContext() context: SharedContext = {}
): Promise<ReservationItem[]> {
const manager = context.transactionManager!
const reservationItemRepository = manager.getRepository(ReservationItem)
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 [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, {
ids: newReservationItems.map((i) => i.id),
})
return newReservationItems
}
/**
* Update a reservation item.
* @param reservationItemId - The reservation item's id.
* @param data - The reservation item data to update.
* @param context
* @return The updated reservation item.
*/
@InjectEntityManager()
async update(
reservationItemId: string,
data: UpdateReservationItemInput,
@MedusaContext() context: SharedContext = {}
): Promise<ReservationItem> {
const manager = context.transactionManager!
const itemRepository = manager.getRepository(ReservationItem)
const item = await this.retrieve(reservationItemId, undefined, context)
const shouldUpdateQuantity =
isDefined(data.quantity) && data.quantity !== item.quantity
const shouldUpdateLocation =
isDefined(data.location_id) && data.location_id !== item.location_id
const ops: Promise<unknown>[] = []
if (shouldUpdateLocation) {
ops.push(
this.inventoryLevelService_.adjustReservedQuantity(
item.inventory_item_id,
item.location_id,
item.quantity * -1,
context
),
this.inventoryLevelService_.adjustReservedQuantity(
item.inventory_item_id,
data.location_id!,
data.quantity || item.quantity!,
context
)
)
} else if (shouldUpdateQuantity) {
const quantityDiff = data.quantity! - item.quantity
ops.push(
this.inventoryLevelService_.adjustReservedQuantity(
item.inventory_item_id,
item.location_id,
quantityDiff,
context
)
)
}
const mergedItem = itemRepository.merge(item, data)
ops.push(itemRepository.save(item))
await Promise.all(ops)
await this.eventBusService_?.emit?.(ReservationItemService.Events.UPDATED, {
id: mergedItem.id,
})
return mergedItem
}
/**
* Deletes a reservation item by line item id.
* @param lineItemId - the id of the line item to delete.
* @param context
*/
@InjectEntityManager()
async deleteByLineItem(
lineItemId: string | string[],
@MedusaContext() context: SharedContext = {}
): Promise<void> {
const manager = context.transactionManager!
const itemRepository = manager.getRepository(ReservationItem)
const lineItemIds = Array.isArray(lineItemId) ? lineItemId : [lineItemId]
const reservationItems = await this.list(
{ line_item_id: lineItemIds },
undefined,
context
)
const ops: Promise<unknown>[] = [
itemRepository.softDelete({ line_item_id: In(lineItemIds) }),
]
for (const reservation of reservationItems) {
ops.push(
this.inventoryLevelService_.adjustReservedQuantity(
reservation.inventory_item_id,
reservation.location_id,
reservation.quantity * -1,
context
)
)
}
await Promise.all(ops)
await this.eventBusService_?.emit?.(ReservationItemService.Events.DELETED, {
line_item_id: lineItemId,
})
}
/**
* Deletes reservation items by location ID.
* @param locationId - The ID of the location to delete reservations for.
* @param context
*/
@InjectEntityManager()
async deleteByLocationId(
locationId: string | string[],
@MedusaContext() context: SharedContext = {}
): Promise<void> {
const manager = context.transactionManager!
const itemRepository = manager.getRepository(ReservationItem)
const ids = Array.isArray(locationId) ? locationId : [locationId]
await itemRepository.softDelete({ location_id: In(ids) })
await this.eventBusService_?.emit?.(ReservationItemService.Events.DELETED, {
location_id: locationId,
})
}
/**
* Deletes a reservation item by id.
* @param reservationItemId - the id of the reservation item to delete.
* @param context
*/
@InjectEntityManager()
async delete(
reservationItemId: string | string[],
@MedusaContext() context: SharedContext = {}
): Promise<void> {
const ids = Array.isArray(reservationItemId)
? reservationItemId
: [reservationItemId]
const manager = context.transactionManager!
const itemRepository = manager.getRepository(ReservationItem)
const items = await this.list({ id: ids }, undefined, context)
const promises: Promise<unknown>[] = items.map(async (item) => {
await this.inventoryLevelService_.adjustReservedQuantity(
item.inventory_item_id,
item.location_id,
item.quantity * -1,
context
)
})
promises.push(itemRepository.softRemove(items))
await Promise.all(promises)
await this.eventBusService_?.emit?.(ReservationItemService.Events.DELETED, {
ids: reservationItemId,
})
}
}