diff --git a/.changeset/shaggy-suits-drop.md b/.changeset/shaggy-suits-drop.md new file mode 100644 index 0000000000..af3b0fcf7d --- /dev/null +++ b/.changeset/shaggy-suits-drop.md @@ -0,0 +1,7 @@ +--- +"@medusajs/core-flows": patch +"@medusajs/medusa": patch +"@medusajs/types": patch +--- + +feat(medusa, core-flows, types): Add delete-location-level api-v2 endpoints diff --git a/integration-tests/modules/__tests__/inventory/index.spec.ts b/integration-tests/modules/__tests__/inventory/index.spec.ts index eee602fc79..553b997fb4 100644 --- a/integration-tests/modules/__tests__/inventory/index.spec.ts +++ b/integration-tests/modules/__tests__/inventory/index.spec.ts @@ -229,6 +229,61 @@ medusaIntegrationTestRunner({ ) }) + describe("Delete inventory levels", () => { + const locationId = "loc_1" + let inventoryItem + + beforeEach(async () => { + inventoryItem = await service.create({ + sku: "MY_SKU", + }) + + await service.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: locationId, + stocked_quantity: 10, + }, + ]) + }) + + it("should delete an inventory location level without reservations", async () => { + const result = await api.delete( + `/admin/inventory-items/${inventoryItem.id}/location-levels/${locationId}`, + adminHeaders + ) + + expect(result.status).toEqual(200) + expect(result.data).toEqual({ + id: expect.any(String), + object: "inventory-level", + deleted: true, + }) + }) + + it("should fail delete an inventory location level with reservations", async () => { + await service.createReservationItems({ + inventory_item_id: inventoryItem.id, + location_id: locationId, + quantity: 5, + }) + + let error + + await api + .delete( + `/admin/inventory-items/${inventoryItem.id}/location-levels/${locationId}`, + adminHeaders + ) + .catch((e) => (error = e)) + expect(error.response.status).toEqual(400) + expect(error.response.data).toEqual({ + type: "not_allowed", + message: `Cannot remove Inventory Level ${inventoryItem.id} at Location ${locationId} because there are reservations at location`, + }) + }) + }) + describe("Retrieve inventory item", () => { let location1 = "loc_1" let location2 = "loc_2" diff --git a/packages/core-flows/src/inventory/steps/delete-inventory-levels.ts b/packages/core-flows/src/inventory/steps/delete-inventory-levels.ts new file mode 100644 index 0000000000..e0f0d7752f --- /dev/null +++ b/packages/core-flows/src/inventory/steps/delete-inventory-levels.ts @@ -0,0 +1,29 @@ +import { ICustomerModuleService, IInventoryServiceNext } from "@medusajs/types" +import { StepResponse, WorkflowData, createStep } from "@medusajs/workflows-sdk" + +import { ModuleRegistrationName } from "@medusajs/modules-sdk" + +export const deleteInventoryLevelsStepId = "delete-inventory-levels-step" +export const deleteInventoryLevelsStep = createStep( + deleteInventoryLevelsStepId, + async (ids: string[], { container }) => { + const service = container.resolve( + ModuleRegistrationName.INVENTORY + ) + + await service.softDeleteInventoryLevels(ids) + + return new StepResponse(void 0, ids) + }, + async (prevLevelIds, { container }) => { + if (!prevLevelIds?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.INVENTORY + ) + + await service.restoreInventoryLevels(prevLevelIds) + } +) diff --git a/packages/core-flows/src/inventory/steps/index.ts b/packages/core-flows/src/inventory/steps/index.ts index 8171259165..832bb448be 100644 --- a/packages/core-flows/src/inventory/steps/index.ts +++ b/packages/core-flows/src/inventory/steps/index.ts @@ -3,3 +3,4 @@ export * from "./create-inventory-items" export * from "./validate-singular-inventory-items-for-tags" export * from "./create-inventory-levels" export * from "./validate-inventory-locations" +export * from "./delete-inventory-levels" diff --git a/packages/core-flows/src/inventory/workflows/delete-inventory-levels.ts b/packages/core-flows/src/inventory/workflows/delete-inventory-levels.ts new file mode 100644 index 0000000000..c6bb338c12 --- /dev/null +++ b/packages/core-flows/src/inventory/workflows/delete-inventory-levels.ts @@ -0,0 +1,15 @@ +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" + +import { deleteInventoryLevelsStep } from "../steps" + +interface WorkflowInput { + ids: string[] +} +export const deleteInventoryLevelsWorkflowId = + "delete-inventory-levels-workflow" +export const deleteInventoryLevelsWorkflow = createWorkflow( + deleteInventoryLevelsWorkflowId, + (input: WorkflowData): WorkflowData => { + return deleteInventoryLevelsStep(input.ids) + } +) diff --git a/packages/core-flows/src/inventory/workflows/index.ts b/packages/core-flows/src/inventory/workflows/index.ts index 46378681d5..04adb040d0 100644 --- a/packages/core-flows/src/inventory/workflows/index.ts +++ b/packages/core-flows/src/inventory/workflows/index.ts @@ -1,2 +1,3 @@ export * from "./create-inventory-items" export * from "./create-inventory-levels" +export * from "./delete-inventory-levels" diff --git a/packages/medusa/src/api-v2/admin/inventory-items/[id]/location-levels/[location_id]/route.ts b/packages/medusa/src/api-v2/admin/inventory-items/[id]/location-levels/[location_id]/route.ts new file mode 100644 index 0000000000..f8394b0b71 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/inventory-items/[id]/location-levels/[location_id]/route.ts @@ -0,0 +1,47 @@ +import { + ContainerRegistrationKeys, + MedusaError, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { MedusaRequest, MedusaResponse } from "../../../../../../types/routing" + +import { deleteInventoryLevelsWorkflow } from "@medusajs/core-flows" + +export const DELETE = async (req: MedusaRequest, res: MedusaResponse) => { + const { id, location_id } = req.params + + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const [{ id: levelId, reserved_quantity: reservedQuantity }] = + await remoteQuery( + remoteQueryObjectFromString({ + entryPoint: "inventory_level", + variables: { + inventory_item_id: id, + location_id, + }, + fields: ["id", "reserved_quantity"], + }) + ) + + if (reservedQuantity > 0) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + `Cannot remove Inventory Level ${id} at Location ${location_id} because there are reservations at location` + ) + } + + const deleteInventoryLevelWorkflow = deleteInventoryLevelsWorkflow(req.scope) + + await deleteInventoryLevelWorkflow.run({ + input: { + ids: [levelId], + }, + }) + + res.status(200).json({ + id: levelId, + object: "inventory-level", + deleted: true, + }) +} diff --git a/packages/medusa/src/api-v2/admin/inventory-items/query-config.ts b/packages/medusa/src/api-v2/admin/inventory-items/query-config.ts index f78976be27..6050e82836 100644 --- a/packages/medusa/src/api-v2/admin/inventory-items/query-config.ts +++ b/packages/medusa/src/api-v2/admin/inventory-items/query-config.ts @@ -1,19 +1,18 @@ import { InventoryNext } from "@medusajs/types" // eslint-disable-next-line max-len -export const defaultAdminLocationLevelFields: (keyof InventoryNext.InventoryLevelDTO)[] = - [ - "id", - "inventory_item_id", - "location_id", - "stocked_quantity", - "reserved_quantity", - "incoming_quantity", - "available_quantity", - "metadata", - "created_at", - "updated_at", - ] +export const defaultAdminLocationLevelFields = [ + "id", + "inventory_item_id", + "location_id", + "stocked_quantity", + "reserved_quantity", + "incoming_quantity", + "available_quantity", + "metadata", + "created_at", + "updated_at", +] export const defaultAdminInventoryItemFields = [ "id", diff --git a/packages/types/src/inventory/service-next.ts b/packages/types/src/inventory/service-next.ts index bc4b20c711..b8be5a8597 100644 --- a/packages/types/src/inventory/service-next.ts +++ b/packages/types/src/inventory/service-next.ts @@ -795,6 +795,17 @@ export interface IInventoryServiceNext extends IModuleService { context?: Context ): Promise + softDeleteInventoryLevels( + inventoryLevelIds: string[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> + + restoreInventoryLevels( + inventoryLevelIds: string[], + config?: RestoreReturn, + sharedContext?: Context + ): Promise | void> /** * This method is used to adjust the inventory level's stocked quantity. The inventory level is identified by the IDs of its associated inventory item and location. *