diff --git a/integration-tests/http/__tests__/inventory/admin/inventory.spec.ts b/integration-tests/http/__tests__/inventory/admin/inventory.spec.ts
index 24cb8f69cc..4064a52bcb 100644
--- a/integration-tests/http/__tests__/inventory/admin/inventory.spec.ts
+++ b/integration-tests/http/__tests__/inventory/admin/inventory.spec.ts
@@ -128,7 +128,7 @@ medusaIntegrationTestRunner({
`/admin/inventory-items/${inventoryItem1.id}/location-levels`,
{
location_id: stockLocation1.id,
- stocked_quantity: 10,
+ stocked_quantity: 0,
},
adminHeaders
)
@@ -138,11 +138,7 @@ medusaIntegrationTestRunner({
const result = await api.post(
`/admin/inventory-items/${inventoryItem1.id}/location-levels/batch`,
{
- create: [
- {
- location_id: "location_2",
- },
- ],
+ create: [{ location_id: "location_2" }],
delete: [stockLocation1.id],
},
adminHeaders
@@ -157,6 +153,28 @@ medusaIntegrationTestRunner({
expect(levelsListResult.status).toEqual(200)
expect(levelsListResult.data.inventory_levels).toHaveLength(1)
})
+
+ it("should not delete an inventory location level when there is stocked items", async () => {
+ await api.post(
+ `/admin/inventory-items/${inventoryItem1.id}/location-levels/${stockLocation1.id}`,
+ { stocked_quantity: 10 },
+ adminHeaders
+ )
+
+ const { response } = await api
+ .post(
+ `/admin/inventory-items/${inventoryItem1.id}/location-levels/batch`,
+ { delete: [stockLocation1.id] },
+ adminHeaders
+ )
+ .catch((e) => e)
+
+ expect(response.status).toEqual(400)
+ expect(response.data).toEqual({
+ type: "not_allowed",
+ message: `Cannot remove Inventory Levels for ${stockLocation1.id} because there are stocked or reserved items at the locations`,
+ })
+ })
})
describe("DELETE /admin/inventory-items/:id/location-levels/:id", () => {
@@ -165,7 +183,7 @@ medusaIntegrationTestRunner({
`/admin/inventory-items/${inventoryItem1.id}/location-levels`,
{
location_id: stockLocation1.id,
- stocked_quantity: 10,
+ stocked_quantity: 0,
},
adminHeaders
)
@@ -187,6 +205,12 @@ medusaIntegrationTestRunner({
})
it("should fail delete an inventory location level with reservations", async () => {
+ await api.post(
+ `/admin/inventory-items/${inventoryItem1.id}/location-levels/${stockLocation1.id}`,
+ { stocked_quantity: 10 },
+ adminHeaders
+ )
+
await api.post(
`/admin/reservations`,
{
@@ -621,11 +645,6 @@ medusaIntegrationTestRunner({
)
).data.reservation
- await api.delete(
- `/admin/inventory-items/${inventoryItem1.id}/location-levels/${item.location_levels[0].id}`,
- adminHeaders
- )
-
await api.delete(
`/admin/reservations/${reservation.id}`,
adminHeaders
@@ -639,7 +658,7 @@ medusaIntegrationTestRunner({
expect(response.data.inventory_item).toEqual(
expect.objectContaining({
id: inventoryItem1.id,
- stocked_quantity: 10,
+ stocked_quantity: 20,
reserved_quantity: 1,
})
)
diff --git a/packages/admin/dashboard/src/routes/inventory/inventory-detail/components/location-levels-table/location-actions.tsx b/packages/admin/dashboard/src/routes/inventory/inventory-detail/components/location-levels-table/location-actions.tsx
index 9c5d6ae8a4..581606b154 100644
--- a/packages/admin/dashboard/src/routes/inventory/inventory-detail/components/location-levels-table/location-actions.tsx
+++ b/packages/admin/dashboard/src/routes/inventory/inventory-detail/components/location-levels-table/location-actions.tsx
@@ -1,10 +1,10 @@
import { PencilSquare, Trash } from "@medusajs/icons"
-import { ActionMenu } from "../../../../../components/common/action-menu"
import { InventoryTypes } from "@medusajs/types"
-import { useDeleteInventoryItemLevel } from "../../../../../hooks/api/inventory"
import { usePrompt } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
+import { ActionMenu } from "../../../../../components/common/action-menu"
+import { useDeleteInventoryItemLevel } from "../../../../../hooks/api/inventory"
export const LocationActions = ({
level,
@@ -51,6 +51,8 @@ export const LocationActions = ({
icon: ,
label: t("actions.delete"),
onClick: handleDelete,
+ disabled:
+ level.reserved_quantity > 0 || level.stocked_quantity > 0,
},
],
},
diff --git a/packages/core/core-flows/src/inventory/steps/delete-levels-by-item-and-location.ts b/packages/core/core-flows/src/inventory/steps/delete-levels-by-item-and-location.ts
deleted file mode 100644
index 1ae842ae12..0000000000
--- a/packages/core/core-flows/src/inventory/steps/delete-levels-by-item-and-location.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { ModuleRegistrationName } from "@medusajs/utils"
-import { createStep, StepResponse } from "@medusajs/workflows-sdk"
-
-import { IInventoryService } from "@medusajs/types"
-import { MedusaError } from "@medusajs/utils"
-
-export const deleteInventoryLevelsFromItemAndLocationsStepId =
- "delete-inventory-levels-from-item-and-location-step"
-/**
- * This step removes one or more inventory levels by their associated inventory item and location.
- */
-export const deleteInventoryLevelsFromItemAndLocationsStep = createStep(
- deleteInventoryLevelsFromItemAndLocationsStepId,
- async (
- input: { inventory_item_id: string; location_id: string }[],
- { container }
- ) => {
- if (!input.length) {
- return new StepResponse(void 0, [])
- }
-
- const service = container.resolve(
- ModuleRegistrationName.INVENTORY
- )
-
- const items = await service.listInventoryLevels({ $or: input }, {})
-
- if (items.some((i) => i.reserved_quantity > 0)) {
- const invalidDeletes = items.filter((i) => i.reserved_quantity > 0)
- throw new MedusaError(
- MedusaError.Types.NOT_ALLOWED,
- `Cannot remove Inventory Levels for ${invalidDeletes
- .map((i) => `Inventory Level ${i.id} at Location ${i.location_id}`)
- .join(
- ", "
- )} because there are reserved quantities for items at locations`
- )
- }
-
- const deletedIds = items.map((i) => i.id)
- await service.softDeleteInventoryLevels(deletedIds)
-
- return new StepResponse(void 0, deletedIds)
- },
- async (prevLevelIds, { container }) => {
- if (!prevLevelIds?.length) {
- return
- }
-
- const service = container.resolve(
- ModuleRegistrationName.INVENTORY
- )
-
- await service.restoreInventoryLevels(prevLevelIds)
- }
-)
diff --git a/packages/core/core-flows/src/inventory/steps/index.ts b/packages/core/core-flows/src/inventory/steps/index.ts
index 03f2fc3ade..ece16087de 100644
--- a/packages/core/core-flows/src/inventory/steps/index.ts
+++ b/packages/core/core-flows/src/inventory/steps/index.ts
@@ -4,7 +4,6 @@ export * from "./create-inventory-items"
export * from "./create-inventory-levels"
export * from "./delete-inventory-items"
export * from "./delete-inventory-levels"
-export * from "./delete-levels-by-item-and-location"
export * from "./update-inventory-items"
export * from "./update-inventory-levels"
export * from "./validate-inventory-locations"
diff --git a/packages/core/core-flows/src/inventory/workflows/bulk-create-delete-levels.ts b/packages/core/core-flows/src/inventory/workflows/bulk-create-delete-levels.ts
index 04fd13de1d..989e5102ab 100644
--- a/packages/core/core-flows/src/inventory/workflows/bulk-create-delete-levels.ts
+++ b/packages/core/core-flows/src/inventory/workflows/bulk-create-delete-levels.ts
@@ -1,13 +1,11 @@
import { InventoryLevelDTO, InventoryTypes } from "@medusajs/types"
import {
+ createWorkflow,
WorkflowData,
WorkflowResponse,
- createWorkflow,
} from "@medusajs/workflows-sdk"
-import {
- createInventoryLevelsStep,
- deleteInventoryLevelsFromItemAndLocationsStep,
-} from "../steps"
+import { createInventoryLevelsStep } from "../steps"
+import { deleteInventoryLevelsWorkflow } from "./delete-inventory-levels"
export interface BulkCreateDeleteLevelsWorkflowInput {
creates: InventoryTypes.CreateInventoryLevelInput[]
@@ -24,7 +22,11 @@ export const bulkCreateDeleteLevelsWorkflow = createWorkflow(
(
input: WorkflowData
): WorkflowResponse => {
- deleteInventoryLevelsFromItemAndLocationsStep(input.deletes)
+ deleteInventoryLevelsWorkflow.runAsStep({
+ input: {
+ $or: input.deletes,
+ },
+ })
return new WorkflowResponse(createInventoryLevelsStep(input.creates))
}
diff --git a/packages/core/core-flows/src/inventory/workflows/delete-inventory-levels.ts b/packages/core/core-flows/src/inventory/workflows/delete-inventory-levels.ts
index ff87b78c54..db44263f24 100644
--- a/packages/core/core-flows/src/inventory/workflows/delete-inventory-levels.ts
+++ b/packages/core/core-flows/src/inventory/workflows/delete-inventory-levels.ts
@@ -1,14 +1,43 @@
import {
+ createStep,
+ createWorkflow,
+ transform,
WorkflowData,
WorkflowResponse,
- createWorkflow,
} from "@medusajs/workflows-sdk"
-import { deleteInventoryLevelsStep } from "../steps"
+import { FilterableInventoryLevelProps } from "@medusajs/types"
+import {
+ deduplicate,
+ MedusaError,
+ ModuleRegistrationName,
+} from "@medusajs/utils"
+import { useRemoteQueryStep } from "../../common"
+import { deleteEntitiesStep } from "../../common/steps/delete-entities"
+
+/**
+ * This step validates that inventory levels are deletable.
+ */
+export const validateInventoryLevelsDelete = createStep(
+ "validate-inventory-levels-delete",
+ async function ({ inventoryLevels }: { inventoryLevels: any[] }) {
+ const undeleteableItems = inventoryLevels.filter(
+ (i) => i.reserved_quantity > 0 || i.stocked_quantity > 0
+ )
+
+ if (undeleteableItems.length) {
+ const stockLocationIds = deduplicate(
+ undeleteableItems.map((item) => item.location_id)
+ )
+
+ throw new MedusaError(
+ MedusaError.Types.NOT_ALLOWED,
+ `Cannot remove Inventory Levels for ${stockLocationIds} because there are stocked or reserved items at the locations`
+ )
+ }
+ }
+)
-export interface DeleteInventoryLevelsWorkflowInput {
- ids: string[]
-}
export const deleteInventoryLevelsWorkflowId =
"delete-inventory-levels-workflow"
/**
@@ -16,7 +45,28 @@ export const deleteInventoryLevelsWorkflowId =
*/
export const deleteInventoryLevelsWorkflow = createWorkflow(
deleteInventoryLevelsWorkflowId,
- (input: WorkflowData): WorkflowResponse => {
- return new WorkflowResponse(deleteInventoryLevelsStep(input.ids))
+ (input: WorkflowData) => {
+ const inventoryLevels = useRemoteQueryStep({
+ entry_point: "inventory_levels",
+ fields: ["id", "stocked_quantity", "reserved_quantity", "location_id"],
+ variables: {
+ filters: input,
+ },
+ })
+
+ validateInventoryLevelsDelete({ inventoryLevels })
+
+ const idsToDelete = transform({ inventoryLevels }, ({ inventoryLevels }) =>
+ inventoryLevels.map((il) => il.id)
+ )
+
+ deleteEntitiesStep({
+ moduleRegistrationName: ModuleRegistrationName.INVENTORY,
+ invokeMethod: "softDeleteInventoryLevels",
+ compensateMethod: "restoreInventoryLevels",
+ data: idsToDelete,
+ })
+
+ return new WorkflowResponse(void 0)
}
)
diff --git a/packages/core/types/src/inventory/common/inventory-level.ts b/packages/core/types/src/inventory/common/inventory-level.ts
index 252a4a26b4..78a43272f2 100644
--- a/packages/core/types/src/inventory/common/inventory-level.ts
+++ b/packages/core/types/src/inventory/common/inventory-level.ts
@@ -65,6 +65,11 @@ export interface InventoryLevelDTO {
*/
export interface FilterableInventoryLevelProps
extends BaseFilterable {
+ /**
+ * Filter inventory levels by the ID
+ */
+ id?: string | string[]
+
/**
* Filter inventory levels by the ID of their associated inventory item.
*/
diff --git a/packages/medusa/src/api/admin/inventory-items/[id]/location-levels/[location_id]/route.ts b/packages/medusa/src/api/admin/inventory-items/[id]/location-levels/[location_id]/route.ts
index 211837afff..00a72f16c4 100644
--- a/packages/medusa/src/api/admin/inventory-items/[id]/location-levels/[location_id]/route.ts
+++ b/packages/medusa/src/api/admin/inventory-items/[id]/location-levels/[location_id]/route.ts
@@ -9,9 +9,9 @@ import {
deleteInventoryLevelsWorkflow,
updateInventoryLevelsWorkflow,
} from "@medusajs/core-flows"
+import { HttpTypes } from "@medusajs/types"
import { refetchInventoryItem } from "../../../helpers"
import { AdminUpdateInventoryLocationLevelType } from "../../../validators"
-import { HttpTypes } from "@medusajs/types"
export const DELETE = async (
req: MedusaRequest,
@@ -45,7 +45,7 @@ export const DELETE = async (
await deleteInventoryLevelWorkflow.run({
input: {
- ids: [levelId],
+ id: [levelId],
},
})