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
This commit is contained in:
Philip Korsholm
2024-03-19 10:40:18 +01:00
committed by GitHub
parent c20eb15cd9
commit 62d5803b20
9 changed files with 270 additions and 127 deletions

View File

@@ -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

View File

@@ -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([])
})
})
})
},

View File

@@ -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)
}
)

View File

@@ -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)
}
)

View File

@@ -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"

View File

@@ -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<string[]>): WorkflowData<string[]> => {
deleteInventoryItemStep(input)
deatachInventoryItemStep(input)
return input
}
)

View File

@@ -1,3 +1,4 @@
export * from "./delete-inventory-items"
export * from "./create-inventory-items"
export * from "./create-inventory-levels"
export * from "./delete-inventory-levels"

View File

@@ -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<InventoryLevel>(this)
@OneToMany(
() => ReservationItem,
(reservationItem) => reservationItem.inventory_item,
{
cascade: ["soft-remove" as any],
}
)
reservation_items = new Collection<ReservationItem>(this)
@Formula(
(item) =>
`(SELECT SUM(reserved_quantity) FROM inventory_level il WHERE il.inventory_item_id = ${item}.id)`,

View File

@@ -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,
})
}