From 62d5803b2085daa682ea9bfbe7a3593057c7da4e Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Tue, 19 Mar 2024 10:40:18 +0100 Subject: [PATCH] Feat(core-flows, medusa): delete inventory item (#6708) * initial get-inventory-item * add exception throw test * remove console log * add changeset * remove inventory item * add changeset * fix pr feedback * use links * use modules instead of property names --- .changeset/silly-cooks-count.md | 7 + .../modules/__tests__/inventory/index.spec.ts | 253 +++++++++--------- .../steps/deatach-inventory-items.ts | 59 ++++ .../inventory/steps/delete-inventory-items.ts | 24 ++ .../core-flows/src/inventory/steps/index.ts | 2 + .../workflows/delete-inventory-items.ts | 13 + .../src/inventory/workflows/index.ts | 1 + .../src/models/inventory-item.ts | 16 +- .../admin/inventory-items/[id]/route.ts | 22 ++ 9 files changed, 270 insertions(+), 127 deletions(-) create mode 100644 .changeset/silly-cooks-count.md create mode 100644 packages/core-flows/src/inventory/steps/deatach-inventory-items.ts create mode 100644 packages/core-flows/src/inventory/steps/delete-inventory-items.ts create mode 100644 packages/core-flows/src/inventory/workflows/delete-inventory-items.ts diff --git a/.changeset/silly-cooks-count.md b/.changeset/silly-cooks-count.md new file mode 100644 index 0000000000..bc2b8e36cb --- /dev/null +++ b/.changeset/silly-cooks-count.md @@ -0,0 +1,7 @@ +--- +"@medusajs/inventory-next": patch +"@medusajs/core-flows": patch +"@medusajs/medusa": patch +--- + +feat(medusa, core-flows, inventory-next): add delete-inventory-item endpoint diff --git a/integration-tests/modules/__tests__/inventory/index.spec.ts b/integration-tests/modules/__tests__/inventory/index.spec.ts index 553b997fb4..9a35c66ecf 100644 --- a/integration-tests/modules/__tests__/inventory/index.spec.ts +++ b/integration-tests/modules/__tests__/inventory/index.spec.ts @@ -615,145 +615,146 @@ medusaIntegrationTestRunner({ }) }) - it.skip("should remove associated levels and reservations when deleting an inventory item", async () => { - const inventoryService = appContainer.resolve("inventoryService") - - const invItem2 = await inventoryService.createInventoryItem({ - sku: "1234567", + describe("delete inventory item", () => { + let invItem + beforeEach(async () => { + invItem = await service.create({ + sku: "MY_SKU", + origin_country: "UK", + hs_code: "hs001", + mid_code: "mids", + material: "material", + weight: 300, + length: 100, + height: 200, + width: 150, + }) }) - const stockRes = await api.post( - `/admin/stock-locations`, - { - name: "Fake Warehouse 1", - }, - adminHeaders - ) + it("should remove associated levels and reservations when deleting an inventory item", async () => { + const inventoryService = appContainer.resolve( + ModuleRegistrationName.INVENTORY + ) - locationId = stockRes.data.stock_location.id + locationId = "location1" - const level = await inventoryService.createInventoryLevel({ - inventory_item_id: invItem2.id, - location_id: locationId, - stocked_quantity: 10, + await inventoryService.createInventoryLevels({ + inventory_item_id: invItem.id, + location_id: locationId, + stocked_quantity: 10, + }) + + await inventoryService.createReservationItems({ + inventory_item_id: invItem.id, + location_id: locationId, + quantity: 5, + }) + + const [, reservationCount] = + await inventoryService.listAndCountReservationItems({ + location_id: locationId, + }) + + expect(reservationCount).toEqual(1) + + const [, inventoryLevelCount] = + await inventoryService.listAndCountInventoryLevels({ + location_id: locationId, + }) + + expect(inventoryLevelCount).toEqual(1) + + const res = await api.delete( + `/admin/inventory-items/${invItem.id}`, + adminHeaders + ) + + expect(res.status).toEqual(200) + + const [, reservationCountPostDelete] = + await inventoryService.listAndCountReservationItems({ + location_id: locationId, + }) + + expect(reservationCountPostDelete).toEqual(0) + + const [, inventoryLevelCountPostDelete] = + await inventoryService.listAndCountInventoryLevels({ + location_id: locationId, + }) + + expect(inventoryLevelCountPostDelete).toEqual(0) }) - const reservation = await inventoryService.createReservationItem({ - inventory_item_id: invItem2.id, - location_id: locationId, - quantity: 5, - }) + it("should remove the product variant associations when deleting an inventory item", async () => { + const secondVariantId = "test-2" + const variantId = "test" - const [, reservationCount] = - await inventoryService.listReservationItems({ - location_id: locationId, - }) + const remoteLinks = appContainer.resolve( + ContainerRegistrationKeys.REMOTE_LINK + ) + const remoteQuery = appContainer.resolve( + ContainerRegistrationKeys.REMOTE_QUERY + ) - 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.skip("should remove the product variant associations when deleting an inventory item", async () => { - await simpleProductFactory( - dbConnection, - { - id: "test-product-new", - variants: [], - }, - 5 - ) - - const response = await api.post( - `/admin/products/test-product-new/variants`, - { - title: "Test2", - sku: "MY_SKU2", - manage_inventory: true, - options: [ - { - option_id: "test-product-new-option", - value: "Blue", + await remoteLinks.create([ + { + productService: { + variant_id: variantId, }, - ], - prices: [{ currency_code: "usd", amount: 100 }], - }, - { headers: { "x-medusa-access-token": "test_token" } } - ) + inventoryService: { + inventory_item_id: invItem.id, + }, + }, + { + productService: { + variant_id: secondVariantId, + }, + inventoryService: { + inventory_item_id: invItem.id, + }, + }, + ]) - const secondVariantId = response.data.product.variants.find( - (v) => v.sku === "MY_SKU2" - ).id - - const inventoryService = appContainer.resolve("inventoryService") - const variantInventoryService = appContainer.resolve( - "productVariantInventoryService" - ) - - const invItem2 = await inventoryService.createInventoryItem({ - sku: "123456", - }) - - await variantInventoryService.attachInventoryItem( - variantId, - invItem2.id, - 2 - ) - await variantInventoryService.attachInventoryItem( - secondVariantId, - invItem2.id, - 2 - ) - - expect( - await variantInventoryService.listInventoryItemsByVariant(variantId) - ).toHaveLength(2) - - expect( - await variantInventoryService.listInventoryItemsByVariant( - secondVariantId + let links = await remoteQuery( + remoteQueryObjectFromString({ + entryPoint: "product_variant_inventory_item", + variables: { + filter: { variant_id: [variantId, secondVariantId] }, + }, + fields: ["variant_id", "inventory_item_id"], + }) ) - ).toHaveLength(2) - await api.delete(`/admin/inventory-items/${invItem2.id}`, { - headers: { "x-medusa-access-token": "test_token" }, - }) - - expect( - await variantInventoryService.listInventoryItemsByVariant(variantId) - ).toHaveLength(1) - - expect( - await variantInventoryService.listInventoryItemsByVariant( - secondVariantId + expect(links).toHaveLength(2) + expect(links).toEqual( + expect.arrayContaining([ + { + variant_id: "test", + inventory_item_id: invItem.id, + }, + { + variant_id: "test-2", + inventory_item_id: invItem.id, + }, + ]) ) - ).toHaveLength(1) + + await api.delete(`/admin/inventory-items/${invItem.id}`, adminHeaders) + + links = await remoteQuery( + remoteQueryObjectFromString({ + entryPoint: "product_variant_inventory_item", + variables: { + filter: { variant_id: [variantId, secondVariantId] }, + }, + fields: ["variant_id", "inventory_item_id"], + }) + ) + + expect(links).toHaveLength(0) + expect(links).toEqual([]) + }) }) }) }, diff --git a/packages/core-flows/src/inventory/steps/deatach-inventory-items.ts b/packages/core-flows/src/inventory/steps/deatach-inventory-items.ts new file mode 100644 index 0000000000..505288df9d --- /dev/null +++ b/packages/core-flows/src/inventory/steps/deatach-inventory-items.ts @@ -0,0 +1,59 @@ +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { ModuleRegistrationName, Modules } from "@medusajs/modules-sdk" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +import { ILinkModule } from "@medusajs/types" + +export const deatachInventoryItemStepId = "deattach-inventory-items-step" +export const deatachInventoryItemStep = createStep( + deatachInventoryItemStepId, + async (ids: string[], { container }) => { + const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK) + + const linkModule: ILinkModule = remoteLink.getLinkModule( + Modules.PRODUCT, + "variant_id", + Modules.INVENTORY, + "inventory_item_id" + ) + + const links = (await linkModule.list( + { inventory_item_id: ids }, + { select: ["variant_id", "inventory_item_id"] } + )) as { inventory_item_id: string; variant_id: string }[] + + await remoteLink.dismiss( + links.map(({ inventory_item_id, variant_id }) => ({ + [Modules.PRODUCT]: { + variant_id, + }, + [Modules.INVENTORY]: { + inventory_item_id, + }, + })) + ) + + return new StepResponse(void 0, links) + }, + async (input, { container }) => { + if (!input?.length) { + return + } + + const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK) + + const linkDefinitions = input.map(({ inventory_item_id, variant_id }) => ({ + [Modules.PRODUCT]: { + variant_id, + }, + [Modules.INVENTORY]: { + inventory_item_id, + }, + })) + + const links = await remoteLink.create(linkDefinitions) + } +) diff --git a/packages/core-flows/src/inventory/steps/delete-inventory-items.ts b/packages/core-flows/src/inventory/steps/delete-inventory-items.ts new file mode 100644 index 0000000000..fff3b689db --- /dev/null +++ b/packages/core-flows/src/inventory/steps/delete-inventory-items.ts @@ -0,0 +1,24 @@ +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +import { ModuleRegistrationName } from "@medusajs/modules-sdk" + +export const deleteInventoryItemStepId = "delete-inventory-item-step" +export const deleteInventoryItemStep = createStep( + deleteInventoryItemStepId, + async (ids: string[], { container }) => { + const inventoryService = container.resolve(ModuleRegistrationName.INVENTORY) + + await inventoryService.softDelete(ids) + + return new StepResponse(void 0, ids) + }, + async (prevInventoryItemIds, { container }) => { + if (!prevInventoryItemIds?.length) { + return + } + + const inventoryService = container.resolve(ModuleRegistrationName.INVENTORY) + + await inventoryService.restore(prevInventoryItemIds) + } +) diff --git a/packages/core-flows/src/inventory/steps/index.ts b/packages/core-flows/src/inventory/steps/index.ts index 832bb448be..182f6a7b31 100644 --- a/packages/core-flows/src/inventory/steps/index.ts +++ b/packages/core-flows/src/inventory/steps/index.ts @@ -1,3 +1,5 @@ +export * from "./delete-inventory-items" +export * from "./deatach-inventory-items" export * from "./attach-inventory-items" export * from "./create-inventory-items" export * from "./validate-singular-inventory-items-for-tags" diff --git a/packages/core-flows/src/inventory/workflows/delete-inventory-items.ts b/packages/core-flows/src/inventory/workflows/delete-inventory-items.ts new file mode 100644 index 0000000000..2dc932b8a3 --- /dev/null +++ b/packages/core-flows/src/inventory/workflows/delete-inventory-items.ts @@ -0,0 +1,13 @@ +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { deatachInventoryItemStep, deleteInventoryItemStep } from "../steps" + +export const deleteInventoryItemWorkflowId = "delete-inventory-item-workflow" +export const deleteInventoryItemWorkflow = createWorkflow( + deleteInventoryItemWorkflowId, + (input: WorkflowData): WorkflowData => { + deleteInventoryItemStep(input) + + deatachInventoryItemStep(input) + return input + } +) diff --git a/packages/core-flows/src/inventory/workflows/index.ts b/packages/core-flows/src/inventory/workflows/index.ts index 04adb040d0..22db2aa542 100644 --- a/packages/core-flows/src/inventory/workflows/index.ts +++ b/packages/core-flows/src/inventory/workflows/index.ts @@ -1,3 +1,4 @@ +export * from "./delete-inventory-items" export * from "./create-inventory-items" export * from "./create-inventory-levels" export * from "./delete-inventory-levels" diff --git a/packages/inventory-next/src/models/inventory-item.ts b/packages/inventory-next/src/models/inventory-item.ts index 8ba25d8a3c..7912089ea3 100644 --- a/packages/inventory-next/src/models/inventory-item.ts +++ b/packages/inventory-next/src/models/inventory-item.ts @@ -1,5 +1,6 @@ import { BeforeCreate, + Cascade, Collection, Entity, Filter, @@ -19,6 +20,7 @@ import { import { DAL } from "@medusajs/types" import { InventoryLevel } from "./inventory-level" +import { ReservationItem } from "./reservation-item" const InventoryItemDeletedAtIndex = createPsqlIndexStatementHelper({ tableName: "inventory_item", @@ -106,10 +108,22 @@ export class InventoryItem { @OneToMany( () => InventoryLevel, - (inventoryLevel) => inventoryLevel.inventory_item + (inventoryLevel) => inventoryLevel.inventory_item, + { + cascade: ["soft-remove" as any], + } ) location_levels = new Collection(this) + @OneToMany( + () => ReservationItem, + (reservationItem) => reservationItem.inventory_item, + { + cascade: ["soft-remove" as any], + } + ) + reservation_items = new Collection(this) + @Formula( (item) => `(SELECT SUM(reserved_quantity) FROM inventory_level il WHERE il.inventory_item_id = ${item}.id)`, diff --git a/packages/medusa/src/api-v2/admin/inventory-items/[id]/route.ts b/packages/medusa/src/api-v2/admin/inventory-items/[id]/route.ts index 1441cb0ded..b5cb180a0b 100644 --- a/packages/medusa/src/api-v2/admin/inventory-items/[id]/route.ts +++ b/packages/medusa/src/api-v2/admin/inventory-items/[id]/route.ts @@ -5,6 +5,8 @@ import { } from "@medusajs/utils" import { MedusaRequest, MedusaResponse } from "../../../../types/routing" +import { deleteInventoryItemWorkflow } from "@medusajs/core-flows" + export const GET = async (req: MedusaRequest, res: MedusaResponse) => { const { id } = req.params const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) @@ -35,3 +37,23 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { inventory_item, }) } + +export const DELETE = async (req: MedusaRequest, res: MedusaResponse) => { + const id = req.params.id + const deleteInventoryItems = deleteInventoryItemWorkflow(req.scope) + + const { errors } = await deleteInventoryItems.run({ + input: [id], + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + res.status(200).json({ + id, + object: "inventory_item", + deleted: true, + }) +}