Fix(inventory, stock-location): Remove orphaned location levels and reservations (#3460)
**What** - Remove related inventory levels and reservation items when a stock location is removed **How** - Add bulk deletion methods for both inventory levels and reservation items to the inventory service api - invoke both on location removal Fixes CORE-1232
This commit is contained in:
1 parent
fe9eea4c18
commit
10bf05c147
8 files changed
+153
-7
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/inventory": patch
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix(inventory, medusa): ensure no orphaned reservations and invenotry levels on location removal
|
||||||
@@ -475,6 +475,71 @@ describe("Inventory Items endpoints", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("When deleting an inventory item it removes associated levels and reservations", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
const inventoryService = appContainer.resolve("inventoryService")
|
||||||
|
|
||||||
|
const invItem2 = await inventoryService.createInventoryItem({
|
||||||
|
sku: "1234567",
|
||||||
|
})
|
||||||
|
|
||||||
|
const stockRes = await api.post(
|
||||||
|
`/admin/stock-locations`,
|
||||||
|
{
|
||||||
|
name: "Fake Warehouse 1",
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
locationId = stockRes.data.stock_location.id
|
||||||
|
|
||||||
|
const level = await inventoryService.createInventoryLevel({
|
||||||
|
inventory_item_id: invItem2.id,
|
||||||
|
location_id: locationId,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
const reservation = await inventoryService.createReservationItem({
|
||||||
|
inventory_item_id: invItem2.id,
|
||||||
|
location_id: locationId,
|
||||||
|
quantity: 5,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [, reservationCount] = await inventoryService.listReservationItems({
|
||||||
|
location_id: locationId,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(reservationCount).toEqual(1)
|
||||||
|
|
||||||
|
const [, inventoryLevelCount] =
|
||||||
|
await inventoryService.listInventoryLevels({
|
||||||
|
location_id: locationId,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(inventoryLevelCount).toEqual(1)
|
||||||
|
|
||||||
|
const res = await api.delete(
|
||||||
|
`/admin/stock-locations/${locationId}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200)
|
||||||
|
|
||||||
|
const [, reservationCountPostDelete] =
|
||||||
|
await inventoryService.listReservationItems({
|
||||||
|
location_id: locationId,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(reservationCountPostDelete).toEqual(0)
|
||||||
|
|
||||||
|
const [, inventoryLevelCountPostDelete] =
|
||||||
|
await inventoryService.listInventoryLevels({
|
||||||
|
location_id: locationId,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(inventoryLevelCountPostDelete).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
it("When deleting an inventory item it removes the product variants associated to it", async () => {
|
it("When deleting an inventory item it removes the product variants associated to it", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -37,7 +37,7 @@ const LocationCard: React.FC<Props> = ({ location }) => {
|
|||||||
const onDelete = async () => {
|
const onDelete = async () => {
|
||||||
const shouldDelete = await dialog({
|
const shouldDelete = await dialog({
|
||||||
heading: "Delete Location",
|
heading: "Delete Location",
|
||||||
text: "Are you sure you want to delete this location",
|
text: "Are you sure you want to delete this location. This will also delete all inventory levels and reservations associated with this location.",
|
||||||
extraConfirmation: true,
|
extraConfirmation: true,
|
||||||
entityName: location.name,
|
entityName: location.name,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -227,6 +227,24 @@ export default class InventoryLevelService extends TransactionBaseService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes inventory levels by location ID.
|
||||||
|
* @param locationId - The ID of the location to delete inventory levels for.
|
||||||
|
*/
|
||||||
|
async deleteByLocationId(locationId: string): Promise<void> {
|
||||||
|
return await this.atomicPhase_(async (manager) => {
|
||||||
|
const levelRepository = manager.getRepository(InventoryLevel)
|
||||||
|
|
||||||
|
await levelRepository.delete({ location_id: locationId })
|
||||||
|
|
||||||
|
await this.eventBusService_
|
||||||
|
.withTransaction(manager)
|
||||||
|
.emit(InventoryLevelService.Events.DELETED, {
|
||||||
|
location_id: locationId,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the total stocked quantity for a specific inventory item at multiple locations.
|
* Gets the total stocked quantity for a specific inventory item at multiple locations.
|
||||||
* @param inventoryItemId - The ID of the inventory item.
|
* @param inventoryItemId - The ID of the inventory item.
|
||||||
|
|||||||
@@ -254,6 +254,18 @@ export default class InventoryService
|
|||||||
.delete(inventoryItemId)
|
.delete(inventoryItemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteInventoryItemLevelByLocationId(locationId: string): Promise<void> {
|
||||||
|
return await this.inventoryLevelService_
|
||||||
|
.withTransaction(this.activeManager_)
|
||||||
|
.deleteByLocationId(locationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteReservationItemByLocationId(locationId: string): Promise<void> {
|
||||||
|
return await this.reservationItemService_
|
||||||
|
.withTransaction(this.activeManager_)
|
||||||
|
.deleteByLocationId(locationId)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes an inventory level
|
* Deletes an inventory level
|
||||||
* @param inventoryItemId - the id of the inventory item associated with the level
|
* @param inventoryItemId - the id of the inventory item associated with the level
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export default class ReservationItemService extends TransactionBaseService {
|
|||||||
CREATED: "reservation-item.created",
|
CREATED: "reservation-item.created",
|
||||||
UPDATED: "reservation-item.updated",
|
UPDATED: "reservation-item.updated",
|
||||||
DELETED: "reservation-item.deleted",
|
DELETED: "reservation-item.deleted",
|
||||||
DELETED_BY_LINE_ITEM: "reservation-item.deleted-by-line-item",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly eventBusService_: IEventBusService
|
protected readonly eventBusService_: IEventBusService
|
||||||
@@ -95,7 +94,10 @@ export default class ReservationItemService extends TransactionBaseService {
|
|||||||
const manager = this.activeManager_
|
const manager = this.activeManager_
|
||||||
const reservationItemRepository = manager.getRepository(ReservationItem)
|
const reservationItemRepository = manager.getRepository(ReservationItem)
|
||||||
|
|
||||||
const query = buildQuery({ id: reservationItemId }, config) as FindManyOptions
|
const query = buildQuery(
|
||||||
|
{ id: reservationItemId },
|
||||||
|
config
|
||||||
|
) as FindManyOptions
|
||||||
const [reservationItem] = await reservationItemRepository.find(query)
|
const [reservationItem] = await reservationItemRepository.find(query)
|
||||||
|
|
||||||
if (!reservationItem) {
|
if (!reservationItem) {
|
||||||
@@ -165,8 +167,7 @@ export default class ReservationItemService extends TransactionBaseService {
|
|||||||
isDefined(data.quantity) && data.quantity !== item.quantity
|
isDefined(data.quantity) && data.quantity !== item.quantity
|
||||||
|
|
||||||
const shouldUpdateLocation =
|
const shouldUpdateLocation =
|
||||||
isDefined(data.location_id) &&
|
isDefined(data.location_id) && data.location_id !== item.location_id
|
||||||
data.location_id !== item.location_id
|
|
||||||
|
|
||||||
const ops: Promise<unknown>[] = []
|
const ops: Promise<unknown>[] = []
|
||||||
|
|
||||||
@@ -243,12 +244,35 @@ export default class ReservationItemService extends TransactionBaseService {
|
|||||||
|
|
||||||
await this.eventBusService_
|
await this.eventBusService_
|
||||||
.withTransaction(manager)
|
.withTransaction(manager)
|
||||||
.emit(ReservationItemService.Events.DELETED_BY_LINE_ITEM, {
|
.emit(ReservationItemService.Events.DELETED, {
|
||||||
line_item_id: lineItemId,
|
line_item_id: lineItemId,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes reservation items by location ID.
|
||||||
|
* @param locationId - The ID of the location to delete reservations for.
|
||||||
|
*/
|
||||||
|
async deleteByLocationId(locationId: string): Promise<void> {
|
||||||
|
return await this.atomicPhase_(async (manager) => {
|
||||||
|
const itemRepository = manager.getRepository(ReservationItem)
|
||||||
|
|
||||||
|
await itemRepository
|
||||||
|
.createQueryBuilder("reservation_item")
|
||||||
|
.softDelete()
|
||||||
|
.where("location_id = :locationId", { locationId })
|
||||||
|
.andWhere("deleted_at IS NULL")
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
await this.eventBusService_
|
||||||
|
.withTransaction(manager)
|
||||||
|
.emit(ReservationItemService.Events.DELETED, {
|
||||||
|
location_id: locationId,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a reservation item by id.
|
* Deletes a reservation item by id.
|
||||||
* @param reservationItemId - the id of the reservation item to delete.
|
* @param reservationItemId - the id of the reservation item to delete.
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { EntityManager } from "typeorm"
|
import { EntityManager } from "typeorm"
|
||||||
import { IStockLocationService } from "../../../../interfaces"
|
import {
|
||||||
|
IInventoryService,
|
||||||
|
IStockLocationService,
|
||||||
|
} from "../../../../interfaces"
|
||||||
import { SalesChannelLocationService } from "../../../../services"
|
import { SalesChannelLocationService } from "../../../../services"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,6 +63,9 @@ export default async (req, res) => {
|
|||||||
"stockLocationService"
|
"stockLocationService"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const inventoryService: IInventoryService =
|
||||||
|
req.scope.resolve("inventoryService")
|
||||||
|
|
||||||
const salesChannelLocationService: SalesChannelLocationService =
|
const salesChannelLocationService: SalesChannelLocationService =
|
||||||
req.scope.resolve("salesChannelLocationService")
|
req.scope.resolve("salesChannelLocationService")
|
||||||
|
|
||||||
@@ -70,6 +76,17 @@ export default async (req, res) => {
|
|||||||
.removeLocation(id)
|
.removeLocation(id)
|
||||||
|
|
||||||
await stockLocationService.withTransaction(transactionManager).delete(id)
|
await stockLocationService.withTransaction(transactionManager).delete(id)
|
||||||
|
|
||||||
|
if (inventoryService) {
|
||||||
|
await Promise.all([
|
||||||
|
inventoryService
|
||||||
|
.withTransaction(transactionManager)
|
||||||
|
.deleteInventoryItemLevelByLocationId(id),
|
||||||
|
inventoryService
|
||||||
|
.withTransaction(transactionManager)
|
||||||
|
.deleteReservationItemByLocationId(id),
|
||||||
|
])
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
res.status(200).send({
|
res.status(200).send({
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ export interface IInventoryService {
|
|||||||
|
|
||||||
deleteInventoryItem(inventoryItemId: string): Promise<void>
|
deleteInventoryItem(inventoryItemId: string): Promise<void>
|
||||||
|
|
||||||
|
deleteInventoryItemLevelByLocationId(locationId: string): Promise<void>
|
||||||
|
|
||||||
|
deleteReservationItemByLocationId(locationId: string): Promise<void>
|
||||||
|
|
||||||
deleteInventoryLevel(
|
deleteInventoryLevel(
|
||||||
inventoryLevelId: string,
|
inventoryLevelId: string,
|
||||||
locationId: string
|
locationId: string
|
||||||
|
|||||||
Reference in new issue
Block a user