fix(core-flows, link-module): product <> inventory delete cascades (#9528)

**What**
- remove cascade delete of inventory items on product delete
- implement inventory deletion in product/variant delete workflows with checks:
  - product/variant cannot be deleted if there are reservations associated with their inventory items
  - inventory item will be cascade deleted if it's not used by other variants (that are not being deleted in the current flow)

---

FIXES CC-581 CC-582
This commit is contained in:
Frane Polić
2024-10-14 16:22:31 +00:00
committed by GitHub
parent 86f744cf3b
commit 809c851865
7 changed files with 355 additions and 31 deletions
@@ -782,7 +782,7 @@ medusaIntegrationTestRunner({
})
describe("DELETE /admin/inventory-items/:id", () => {
it("should remove associated levels and reservations when deleting an inventory item", async () => {
it("should throw if inventory item with reservations is being removed", async () => {
await api.post(
`/admin/inventory-items/${inventoryItem1.id}/location-levels`,
{
@@ -821,30 +821,16 @@ medusaIntegrationTestRunner({
).data
expect(levelsResponse.count).toEqual(1)
const res = await api.delete(
`/admin/inventory-items/${inventoryItem1.id}`,
adminHeaders
const res = await api
.delete(`/admin/inventory-items/${inventoryItem1.id}`, adminHeaders)
.catch((err) => {
return err.response
})
expect(res.status).toEqual(400)
expect(res.data.message).toEqual(
`Cannot remove following inventory item(s) since they have reservations: [${inventoryItem1.id}].`
)
expect(res.status).toEqual(200)
const reservationsResponseAfterDelete = (
await api.get(
`/admin/reservations?location_id[]=${stockLocation1.id}`,
adminHeaders
)
).data
expect(reservationsResponseAfterDelete.count).toEqual(0)
const levelsResponseAfterDelete = (
await api.get(
`/admin/inventory-items/${inventoryItem1.id}/location-levels?location_id[]=${stockLocation1.id}`,
adminHeaders
)
).data
expect(levelsResponseAfterDelete.count).toEqual(0)
})
it("should remove the product variant associations when deleting an inventory item", async () => {
@@ -2379,6 +2379,218 @@ medusaIntegrationTestRunner({
expect(variantPost).not.toBeTruthy()
})
it("deletes product and inventory items that are only associated with that product's variants", async () => {
const stockLocation = (
await api.post(
`/admin/stock-locations`,
{ name: "loc" },
adminHeaders
)
).data.stock_location
const inventoryItem1 = (
await api.post(
`/admin/inventory-items`,
{ sku: "inventory-1" },
adminHeaders
)
).data.inventory_item
const inventoryItem2 = (
await api.post(
`/admin/inventory-items`,
{ sku: "inventory-2-reused-across-products" },
adminHeaders
)
).data.inventory_item
await api.post(
`/admin/inventory-items/${inventoryItem1.id}/location-levels`,
{
location_id: stockLocation.id,
stocked_quantity: 8,
},
adminHeaders
)
await api.post(
`/admin/inventory-items/${inventoryItem2.id}/location-levels`,
{
location_id: stockLocation.id,
stocked_quantity: 4,
},
adminHeaders
)
const productWithInventoryItems = (
await api.post(
`/admin/products`,
{
title: "Test product - 1",
handle: "test-1",
options: [{ title: "size", values: ["l"] }],
variants: [
{
title: "Custom inventory 1",
prices: [{ currency_code: "usd", amount: 100 }],
manage_inventory: true,
options: { size: "l" },
inventory_items: [
{
inventory_item_id: inventoryItem1.id,
required_quantity: 4,
},
{
inventory_item_id: inventoryItem2.id,
required_quantity: 2,
},
],
},
],
},
adminHeaders
)
).data.product
// Another product that shares inventory item in the inventory kit
await api.post(
`/admin/products`,
{
title: "Test product - 2",
handle: "test-2",
options: [{ title: "size", values: ["l"] }],
variants: [
{
title: "W/ shared inventory item",
prices: [{ currency_code: "usd", amount: 100 }],
manage_inventory: true,
options: { size: "l" },
inventory_items: [
{
inventory_item_id: inventoryItem2.id,
required_quantity: 2,
},
],
},
],
},
adminHeaders
)
const response = await api
.delete(
`/admin/products/${productWithInventoryItems.id}`,
adminHeaders
)
.catch((err) => {
console.log(err)
})
expect(response.status).toEqual(200)
expect(response.data).toEqual(
expect.objectContaining({ deleted: true })
)
const item1Response = await api
.get(`/admin/inventory-items/${inventoryItem1.id}`, adminHeaders)
.catch((err) => {
return err.response
})
const item2Response = await api
.get(`/admin/inventory-items/${inventoryItem2.id}`, adminHeaders)
.catch((err) => {
console.log(err)
})
expect(item1Response.status).toEqual(404) // deleted since it's used only by the deleted product
expect(item2Response.status).toEqual(200) // not deleted since it belongs to other products
expect(item2Response.data.inventory_item).toEqual(
expect.objectContaining({ id: inventoryItem2.id })
)
})
it("should throw if product that has a reservation is being deleted", async () => {
const stockLocation = (
await api.post(
`/admin/stock-locations`,
{ name: "loc" },
adminHeaders
)
).data.stock_location
const inventoryItem1 = (
await api.post(
`/admin/inventory-items`,
{ sku: "inventory-1" },
adminHeaders
)
).data.inventory_item
await api.post(
`/admin/inventory-items/${inventoryItem1.id}/location-levels`,
{
location_id: stockLocation.id,
stocked_quantity: 8,
},
adminHeaders
)
const productWithInventoryItems = (
await api.post(
`/admin/products`,
{
title: "Test product - 1",
handle: "test-1",
options: [{ title: "size", values: ["l"] }],
variants: [
{
title: "Custom inventory 1",
prices: [{ currency_code: "usd", amount: 100 }],
manage_inventory: true,
options: { size: "l" },
inventory_items: [
{
inventory_item_id: inventoryItem1.id,
required_quantity: 4,
},
],
},
],
},
adminHeaders
)
).data.product
const reservation = (
await api.post(
`/admin/reservations`,
{
line_item_id: "line-item-id-1",
inventory_item_id: inventoryItem1.id,
location_id: stockLocation.id,
description: "test description",
quantity: 1,
},
adminHeaders
)
).data.reservation
const response = await api
.delete(
`/admin/products/${productWithInventoryItems.id}`,
adminHeaders
)
.catch((err) => {
return err.response
})
expect(response.status).toEqual(400)
expect(response.data.message).toEqual(
`Cannot remove following inventory item(s) since they have reservations: [${inventoryItem1.id}].`
)
})
// TODO: Enable with http calls
it.skip("successfully deletes a product variant and its associated prices", async () => {
// // Validate that the price exists